지금까지 한 것: 요구사항 만족 → 파라미터 넘기는 불편함 때문에 쓰레드 로컬 도입
그런데 로그 추적기 도입 전(V0)에는 핵심 기능만 깔끔하게 남아있고 도입 후(V3)에는 핵심 기능보다 로그를 출력해야 하는 부가 기능 코드가 훨씬 많고 복잡하다.
Ex. orderService
의 핵심 기능은 주문 로직
메소드 단위로 보면 orderService.orderItem()
의 핵심 기능은 주문 데이터를 저장하기 위해 리포지토리를 호출하는 orderRepository.save(itemId)
코드가 핵심 기능
Ex. 로그 추적 로직, 트랜잭션 기능
이러한 부가 기능은 단독으로 사용되지 않고 핵심 기능과 함께 사용된다.
(로그 추적 기능은 어떤 핵심 기능이 호출되었는지 로그를 남기기 위해 사용)
다시 말하자면 V0은 핵심 기능만 있지만, 로그 추적기를 추가한 V3 코드는 핵심 기능과 부가 기능이 섞인 상태이다. V3를 보면 로그 추적기의 도입으로 부가 기능 처리 코드 >>>>>> 핵심 기능 코드
가 되었다.
배꼽이 배보다 커졌다 ⛴
V3의 코드를 보면 아래와 같은 동일한 패턴을 발견할 수 있다.
controller
, Service
, Repository
의 코드를 보면 로그 추적기를 사용하는 구조는 모두 동일하다.
중간 핵심 기능 코드만 다를 뿐이다.
부가 기능과 관련된 코드가 중복이니 중복을 별도의 메소드로 뽑아내면 될 것 같지만 try~catch
는 물론, 핵심 기능 부분이 중간에 있어 단순히 메소드로 추출하는 것은 어렵다.
좋은 설계는 변하는 것과 변하지 않는 것을 분리하는 것이다.
여기서 핵심 기능 부분은 변하고, 로그 추적기를 사용하는 부분은 변하지 않는 부분이다.
이 둘을 분리해 모듈화해야 한다.
템플릿 메소드 패턴(Template Method Pattern)은 이런 문제를 해결하는 디자인 패턴이다.
템플릿 메소드 패턴을 이해하기 위해 단순한 예제 코드 작성하기
TemplateMethodTest.java
@Slf4j
public class TemplateMethodTest {
@Test
void templateMethodV0() {
logic1();
logic2();
}
private void logic1() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직1 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
private void logic2() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직2 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
변하는 부분: 비즈니스 로직
변하지 않는 부분: 시간 측정
@Slf4j
public abstract class AbstractTemplate {
public void execute() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
call(); // 상속
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
protected abstract void call(); // 변하는 부분은 자식 클래스에 따라 바뀌는
}
템플릿 메소드 패턴
은 이름 그대로 템플릿을 사용하는 방식이다. 템플릿은 기준이 되는 거대한 틀이다.
템플릿이라는 틀에 변하지 않는 부분을 몰아둔다. 그리고 일부 변하는 부분을 별도로 호출해 해결한다.
AbstractTemplate
코드를 보면 변하지 않는 부분인 시간 측정 로직을 몰아두었다.
이제 이것이 하나의 템플릿이 된다. 그리고 템플릿 안에서 변하는 부분은 call()
메소드를 호출해 처리한다.
템플릿 메소드 패턴은 부모 클래스에 변하지 않는 템플릿 코드를 둔다. 그리고 변하는 부분은 자식 클래스에 두고 상속과 오버라이딩을 사용해 처리한다.
@Slf4j
public class SubClassLogic1 extends AbstractTemplate{
@Override
protected void call() {
log.info("비즈니스 로직1 실행");
}
}
SubClassLogic2.java
도 1과 코드는 같고 1을 2로만 바꾸어주기
변하는 부분인 비즈니스 로직1을 처리하는 자식 클래스
템플릿이 호출하는 대상인 call()
메소드를 오버라이딩
/**
* 템플릿 메소드 패턴 적용
* 코드 중복 제거됨
*/
@Test
void templateMethodV1() {
AbstractTemplate template1 = new SubClassLogic1();
template1.execute();
AbstractTemplate template2 = new SubClassLogic2();
template2.execute();
}
변경이 용이하다는 장점이 생겼다. 예를 들어 resultTime
을 최종결과
라고 바꾸려고 할 때 예제 1과는 다르게 한 번만에 바꿀 수 있다.
resultTime
이라고 쓰인 부분을 모조리 찾아 직접 바꿔줄 필요가 사라졌다.
➡️ 단일 체계 원칙이 잘 지켜지고 있음
template1.execute()
를 호출하면 템플릿 로직인 AbstractTemplate.execute()
를 실행한다.
여기서 중간에 call()
메소드를 호출하는데, 이 부분이 오버라이딩 되어있다. 따라서 현재 인스턴스인 SubClassLogic1
인스턴스의 SubClassLogic1.call()
메소드가 호출된다.
템플릿 메소드 패턴은 이렇게 다형성을 사용해 변하는 부분과 변하지 않는 부분을 분리하는 방법이다.
템플릿 메소드 패턴은 SubClassLogic1
, SubClassLogic2
처럼 클래스를 계속 만들어야 한다는 단점이 있다.
📌익명 내부 클래스를 사용하면 이러한 단점을 보완할 수 있다.
익명 내부 클래스를 사용하면 객체 인스턴스를 생성하면서 동시에 생성할 클래스를 상속 받은 자식 클래스를 정의할 수 있다. 이 클래스는 SubClassLogic1
처럼 직접 지정하는 이름이 없고 클래스 내부에 선언되는 클래스여서 익명 내부 클래스라고 한다.
📌 익명 내부 클래스
- 일반적인 형태:
객체 = new 인스턴스();
- 익명 클래스:
객체 = new 인스턴스() { 해당 인스턴스가 가진 특정 메소드를 재정의하여 사용 };
익명 클래스는 인스턴스가 없으면 만들 수 없다. (숙주가 필요. 쉽게 말해 인스턴스에 기생)
그리고 기존 클래스가 가진 메소드를재정의
하기 위해 익명 클래스를 사용한다.
@Test
void templateMethodV2() {
AbstractTemplate template = new AbstractTemplate() {
@Override
protected void call() {
log.info("비즈니스 로직1 실행");
}
};
log.info("클래스 이름1={}", template.getClass());
template.execute();
AbstractTemplate template2 = new AbstractTemplate() {
@Override
protected void call() {
log.info("비즈니스 로직2 실행");
}
};
log.info("클래스 이름2={}", template2.getClass());
template2.execute();
}
실행 결과를 보면 자바가 임의로 만들어주는 익명 내부 클래스 이름은 TemplateMethodTest$1
, TemplateMethodTest$2
인 것을 확인할 수 있다.