템플릿 메서드 패턴

Hoo-Sung.Lee·2024년 4월 15일
0

Spring

목록 보기
6/15

템플릿 메서드 패턴이란?

GOF 디자인 패턴에서는 템플릿 메서드 패턴을 다음과 같이 정의했다.

템플릿 메서드 디자인 패턴의 목적은 다음과 같습니다.
"작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기합니다. 템플릿 메서드를 사용하면 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의할 수 있습니다.

위의 설명만 듣고서는 이해하기가 어려웠다. 아래의 코드는 로그 출력기를 controller, service, repository계층에 모두 있는 코드이다.

public class OrderControllerV3 {

    private final OrderServiceV3 orderService;
    private final LogTrace trace;

    @GetMapping("/v3/request")
    public String request(String itemId) {

        TraceStatus status = null;
        try {
            status = trace.begin("OrderController.request()");
            orderService.orderItem(itemId);
            trace.end(status);
            return "ok";
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;//예외를 꼭 다시 던져주어야 한다.
        }
    }
}
public class OrderServiceV3 {

    private final OrderRepositoryV3 orderRepository;
    private final LogTrace trace;

    public void orderItem(String itemId) {

        TraceStatus status = null;
        try {
            status = trace.begin("OrderService.orderItem()");
            orderRepository.save(itemId);
            trace.end(status);
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;//예외를 꼭 다시 던져주어야 한다.
        }
    }
}
public class OrderRepositoryV3 {

    private final LogTrace trace;
    public void save(String itemId) {
        //저장 로직
        TraceStatus status = null;
        try {
            status = trace.begin("OrderRepository.save()");

            if (itemId.equals("ex")) {
                throw new IllegalStateException("예외 발생!");
            }
            sleep(1000);

            trace.end(status);
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;//예외를 꼭 다시 던져주어야 한다.
        }

    }

위의 로직들을 보면, 로그를 남기기 위한 하나의 목적을 위한 코드들이 공통적으로 작성되어 있다. 심지어 try catch 구문으로 더럽다..

Controller에서는 orderService.orderItem,
Service에서는 orderRepository.save(itemId),
Repository에서는 저장하는 로직이 핵심이다.

로그를 남기는 부분을 템플릿화 하여, 변하지 않는 부분(로그 남기는 부분)과 변하는 부분(중요 로직)을 분리해서 모듈화 하는 것이 템플릿 메서드 패턴이라고 할 수 있다.

이는 SRP(단일 책임 원칙)을 지킬 수 있는 좋은 설계를 위한 길이라고 한다.
단일 책임 원칙을 지키면, 변하지 않는 부분(템플릿 화 한 내용)이 바뀌게 되는 경우에, 하나의 파일만 수정해주면 된다. 하지만 이를 지키지 않은 경우, 모든 파일을 직접 하나하나 바꿔야 하는 번거로움이 생긴다.

이렇듯 좋은 설계란 변경이 일어났을 때, 자연스럽게 드러난다고 한다.

간단한 예시 코드

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();
}
public class SubClassLogic1 extends AbstractTemplate{
    @Override
    protected void call() {
        log.info("비즈니스 로직1 실행");
    }
}
    @Test
    void templateMethodV1() {
        AbstractTemplate template1 = new SubClassLogic1();
        template1.execute();

        AbstractTemplate template2 = new SubClassLogic2();
        template2.execute();
    }
  • Template으로 여러 부분에서 공통으로 사용되는 변하지 않는 부분을 만들어 놓는다.
  • AbstractTemplate을 상속받은 자식 클래스를 만들어, 변하는 부분(call)의 로직을 작성한다.
  • Test 코드와 같이 실행을 하면, 정상 작동한다.

익명 내부 클래스 사용하기

탬플릿 메서드 패턴은 SubClassLogic1, SubClassLogic2 처럼 클래스를 계속 만들어야 하는 단점이 있다. 익명 내부 클래스를 사용하면 이런 단점을 보완할 수 있다.

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

TemplateMethodTest

@Test
    void templateMethodV2() {
        AbstractTemplate template1 = new AbstractTemplate() {
            @Override
            protected void call() {
                log.info("비즈니스 로직1 실행");
            }
        };
        log.info("클래스 이름={}",template1.getClass());
        template1.execute();

        AbstractTemplate template2 = new AbstractTemplate() {
            @Override
            protected void call() {
                log.info("비즈니스 로직2 실행");
            }
        };
        log.info("클래스 이름={}",template2.getClass());
        template2.execute();
    }

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


템플릿 메소드 패턴의 한계

부모 클래스에 알고리즘의 골격인 템플릿을 정의하고, 일부 변경되는 로직을 자식 클래스에 정의하는 템플릿 메소드 패턴은, 자식 클래스가 알고리즘의 전체 구조를 변경하지 않고, 특정 부분만 재정의할 수 있다. 결국 상속과 오버라이딩을 통한 다형성으로 문제를 해결하는 것이다.

하지만, 템플릿 메서드 패턴은 상속을 사용한다.
따라서 상속에서 오는 단점들을 그대로 안고간다.
특히 자식 클래스가 부모 클래스와 컴파일 시점에 강하게 결합되는 문제가 있다. 이것은 의존관계에 대한 문제이다. 자식 클래스 입장에서는 부모 클래스의 기능을 전혀 사용하지 않는다.

코드를 보면, 자식 클래스의 extends 다음에 부모 클래스가 코드상에 지정되어 있다. 따라서 부모 클래스의 기능을 사용하지 않아도 부모 클래스에 강하게 의존하게 된다.

이것은 좋은 설계가 아니다. 이런 잘못된 의존관계 때문에 부모 클래스를 수정하면, 자식 클래스에도 영향을 미친다.

템플릿 메소드 패턴과 비슷한 역할을 하면서 상속의 단점을 제거할 수 있는 디자인 패턴이 전략 패턴이다(Strategy Pattern).

[Effective Java] Item 18: "상속보다는 컴포지션을 사용해라" 를 공부해보면 좋을 것 같다. 다음 포스팅에서!!


참고자료: 김영한 스프링 고급편

profile
Software Engineer

0개의 댓글

관련 채용 정보