5 분 소요

 인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.

템플릿 메서드 패턴 - 시작

지금까지 로그 추적기를 잘 만들었다. 요구 사항도 만족하고, 파라미터를 넘기는 불편함을 제거하기 위해 쓰레드 로컬도 도입했다.

그런데 막상 프로젝트에 도입하려고 하니 개발자들의 반대의 목소리가 높다.

도입 전과 후로 코드를 비교해 보자.

로그 추적 도입 전 - v0

1
2
3
4
5
@GetMapping("/v0/request")  
public String request(@RequestParam("itemId") String itemId) {  
    orderService.orderItem(itemId);  
    return "ok";  
}

로그 추적 도입 후 - v3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final LogTrace trace;  
  
@GetMapping("/v3/request")  
public String request(@RequestParam("itemId") String itemId) {  
  
    TraceStatus status = null;  
    try {  
        status = trace.begin("OrderControllerV3.request()");  
        orderService.orderItem(itemId);  
        trace.end(status);  
        return "ok";  
  
    } catch (Exception e) {  
        trace.exception(status, e);  
        throw e;  
    }  
}

V0는 해당 메서드가 실제 처리해야 하는 핵심 기능만 깔끔하게 남아있다. 반면에 V3에는 핵심 기능보다 로그를 출력해야 하는 부가 기능 코드가 훨씬 많고 복잡하다.

핵심 기능 vs 부가 기능

  • 핵심 기능은 해당 객체가 제공하는 고유의 기능이다. 예를 들어서 orderService 의 핵심 기능은 주문 로직이다. 메서드 단위로 보면 orderService.orderItem()의 핵심 기능은 주문 데이터를 저장하기 위해 리포지토리를 호출하는 orderRepository.save(itemId)코드가 핵심이다.
  • 부가 기능은 핵심 기능을 보조하기 위해 제공되는 기능이다. 예를 들어서 로그 추적 로직, 트랜잭션 기능이 있다. 이러한 부가 기능은 단독으로 사용되지는 않고, 핵심 기능과 함께 사용된다. 예를 들어서 로 추적 기능은 어떤 핵심 기능이 호출되었는지 로그를 남기기 위해 사용한다.

V0는 핵심 기능만 있지만, 로그 추적기를 추가한 V3코드는 핵심 기능과 부가 기능이 함께 섞여있다.

V3를 보면 로그 추적기의 도입으로 핵심 기능 코드보다 부가 기능을 처리하기 위한 코드가 더 많아졌다. 소위 배보다 배꼽이 더 큰 상황이다. 만약 클래스가 수백 개라면?

1
2
3
4
5
6
7
8
9
10
11
TraceStatus status = null;  
try {  
	status = trace.begin("OrderControllerV3.request()");  
	orderService.orderItem(itemId);  
	trace.end(status);  
	return "ok";  

} catch (Exception e) {  
	trace.exception(status, e);  
	throw e;  
} 

Controller, Service, Repository의 코드를 잘 보면, 로그 추적기를 사용하는 구조는 다 동일하다.

중간에 핵심 기능을 호출하는 코드만 다를 뿐이다.

부가 기능과 관련된 코드가 중복이니 중복을 별도의 메서드로 뽑아내면 될 것 같다. 그런데, try ~ catch는 물론이고, 핵심 기능 부분이 중간에 있어서 단순하게 메서드로 추출하는 것은 어렵다.

변하는 것과 변하지 않는 것을 분리

좋은 설계는 변하는 것과 변하지 않는 것을 분리하는 것이다.

여기서 핵심 기능 부분은 변하고, 로그 추적기를 사용하는 부분은 변하지 않는 부분이다.

이 둘을 분리해서 모듈화해야 한다

템플릿 메서드 패턴(Template Method Pattern)은 이런 문제를 해결하는 디자인 패턴이다.

템플릿 메서드 패턴 - 예제1

템플릿 메서드 패턴을 쉽게 이해하기 위해 단순한 예제 하나 만들어 보자.

다음과 같이 template패키지를 테스트 영역에 만들고 TemplateMethodTest를 만들었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@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);  
    }  
}

logic1(), logic2()를 호출하는 단순한 코드이다.

logic1()logic2()시간을 측정하는 부분비즈니스 로직을 실행하는 부분이 함께 존재한다.

  • 변하는 부분 : 비즈니스 로직
  • 변하지 않는 부분 : 시간 측정

이제 템플릿 메서드 패턴을 사용해서 변하는 부분과 변하지 않는 부분을 분리해 보자.

템플릿 메서드 패턴 - 예제2

execute()에서 공통 부분을 다 처리하고 call()을 핵심 로직으로 구현한다.

말로 살짝 이해가 안 가니 코드로 보자.

(test 쪽 패키지이다!)

hello.advanced.trace.template.code.AbstractTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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() 메서드를 호출해서 처리한다.

템플릿 메서드 패턴은 부모 클래스에 변하지 않는 템플릿 코드를 둔다.

그리고 변하는 부분은 자식 클래스에 두고 상속과 오버라이딩을 사용해서 처리한다.

SubClassLogic

주의: 테스트 코드(src/test)에 위치한다.

hello.advanced.trace.template.code.SubClassLogic1

1
2
3
4
5
6
7
@Slf4j  
public class SubClassLogic1 extends AbstractTemplate{  
    @Override  
    protected void call() {  
        log.info("비즈니스 로직 1 실행");      
    }  
}

hello.advanced.trace.template.code.SubClassLogic2

1
2
3
4
5
6
7
8
@Slf4j  
public class SubClassLogic2 extends AbstractTemplate{  
  
    @Override  
    protected void call() {  
        log.info("비즈니스 로직 2 실행");  
    }  
}

이제 테스트 코드를 작성해서 실행해 보자.

1
2
3
4
5
6
7
8
9
10
11
/**  
 * 템플릿 메서드 패턴 적용  
 * */  
@Test  
void templateMethodV1() {  
    AbstractTemplate template1 = new SubClassLogic1();  
    template1.execute();  
  
    AbstractTemplate template2 = new SubClassLogic2();  
    template2.execute();  
}

자 똑같은 결과가 실행됐다.

template1.execute()를 호출하면 템플릿 로직인 AbstractTemplate.execute()를 실행한다. 여기서 중간에 call()메서드를 호출하는데, 이 부분이 오버라이딩 되어있다.

따라서 현재 인스턴스인 SubClassLogic1인스턴스의 call()이 호출 된다.

템플릿 메서드 패턴은 이렇게 다형성을 사용해서 변하는 부분과 변하지 않는 부분을 분리하는 방법이다.

템플릿 메서드 패턴 - 예제3 익명 내부 클래스 사용하기

템플릿 메서드 패턴은 SubClassLogic1, SubClassLogic2처럼 클래스를 계속 만들어야 하는 단점이 있다.

익명 내부 클래스를 사용하면 이런 단점을 보완할 수 있다.

익명 내부 클래스를 사용하면 객체 인스턴스를 생성하면서 동시에 생성할 클래스를 상속 받은 자식 클래스를 정의할 수 있다. 이 클래스는 SubClassLogic1처럼 직접 지정하는 이름이 없고 클래스 내부에 선언되는 클래스여서 익명 내부 클래스라 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test  
void templateMethodV2() {  
    AbstractTemplate template1 = new AbstractTemplate() {  
        @Override  
        protected void call() {  
            log.info("비즈니스 로직 1 실행");  
        }  
    };  
    log.info("클래스 이름 1 = {}", template1.getClass());  
    template1.execute();  
  
    AbstractTemplate template2 = new AbstractTemplate() {  
        @Override  
        protected void call() {  
            log.info("비즈니스 로직 2 실행");  
        }  
    };  
    log.info("클래스 이름 2 = {}", template2.getClass());  
    template2.execute();  
}

실행 결과를 보면 자바가 임의로 만들어주는 익명 내부 클래스 이름은 TemplateMethodTest$1, TemplateMethodTest$2인 것을 확인할 수 있다.

댓글남기기