템플릿 메서드 패턴

업데이트:

템플릿 메서드 패턴(Template Method Pattern)이란?

  • 어떤 작업을 처리하는 부분을 서브 클래스로 캡슐화해 구조는 그대로 두고 특정 부분만 변경할 수 있는 패턴이다.
  • 작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기한다.
  • 템플릿 메서드를 사용하면 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의할 수 있다. (GOF 디자인 패턴)

테스트를 통한 패턴 학습


@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();
}

Untitled

템플릿 메서드 패턴을 위한 코드와 구조 그림이다. 공통되는 로직은 execute() 메서드에 템플릿(틀)으로 두고 자식 클래스가 call() 메서드를 구현해서 처리한다.


@Slf4j
public class SubClassLogic1 extends AbstractTemplate {

    @Override
    protected void call() {
        log.info("비즈니스 로직1 실행");
    }
}

@Slf4j
public class SubClassLogic2 extends AbstractTemplate {

    @Override
    protected void call() {
        log.info("비즈니스 로직2 실행");
    }
}

@Slf4j
public class TemplateMethodTest {
    @Test
    void templateMethodV1() {
        AbstractTemplate template1 = new SubClassLogic1();
        template1.execute();
        AbstractTemplate template2 = new SubClassLogic2();
        template2.execute();
    }
}

Untitled

이렇게 자식 클래스들이 call 부분 구현을 통해 각각의 다른 로직에 원하는 공통된 부가 기능을 추가할 수 있었다. 하지만 매번 추가 클래스를 만들어야 한다는 번거로움이 있다. 이를 보완하기 위해 익명 내부 클래스를 사용할 수 있다.


@Slf4j
public class TemplateMethodTest {
    @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();
    }
}

SubClassLogic1SubClassLogic2 대신 익명 내부 클래스를 이용해 바로 선언해서 사용할 수 있다.

핵심 로직에 적용

코드를 통해 패턴을 살펴보자. 현재 주문을 받는 간단한 api가 하나 구현되어 있다.


@RestController
@RequiredArgsConstructor
public class OrderControllerV0 {

    private final OrderServiceV0 orderService;

    @GetMapping("/v0/request")
    public String request(String itemId) {
        orderService.orderItem(itemId);
        return "ok";
    }
}

@Service
@RequiredArgsConstructor
public class OrderServiceV0 {

    private final OrderRepositoryV0 orderRepository;

    public void orderItem(String itemId) {
        orderRepository.save(itemId);
    }
}

@Repository
@RequiredArgsConstructor
public class OrderRepositoryV0 {

    public void save(String itemId) {
        //저장 로직
        if (itemId.equals("ex")) {
            throw new IllegalStateException("예외 발생!");
        }
        sleep(1000);
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

이 코드에 메서드마다 실행 시간을 체크하고 예외가 발생하는 경우 예외를 띄우는 로그를 작성하려고 한다. 매 코드마다 작성하는 방법도 있지만 요구사항이 바뀌는 경우 일일히 수정을 해야 하는 번거로움이 있다. 이를 해결하기 위해 위에서 학습한 템플릿 메서드 패턴을 적용해보자. 익명 내부 클래스를 적용해보았다.


@RestController
@RequiredArgsConstructor
public class OrderControllerV4 {

    private final OrderServiceV4 orderService;
    private final LogTrace trace;

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

        AbstractTemplate<String> template = new AbstractTemplate<>(trace) {
            @Override
            protected String call() {
                orderService.orderItem(itemId);
                return "ok";
            }
        };
        return template.execute("OrderController.request()");
    }
}

@Service
@RequiredArgsConstructor
public class OrderServiceV4 {

    private final OrderRepositoryV4 orderRepository;
    private final LogTrace trace;

    public void orderItem(String itemId) {
        AbstractTemplate<Void> template = new AbstractTemplate<>(trace) {
            @Override
            protected Void call() {
                orderRepository.save(itemId);
                return null;
            }
        };
        template.execute("OrderService.orderItem()");
    }
}

@Repository
@RequiredArgsConstructor
public class OrderRepositoryV4 {

    private final LogTrace trace;

    public void save(String itemId) {
        AbstractTemplate<Void> template = new AbstractTemplate<>(trace) {
            @Override
            protected Void call() {
                //저장 로직
                if (itemId.equals("ex")) {
                    throw new IllegalStateException("예외 발생!");
                }
                sleep(1000);
                return null;
            }
        };
        template.execute("OrderRepository.save()");

    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

각각의 클래스에서 call()을 @Override 해서 핵심 기능을 넣어주었다. 이를 통해 공통된 부가 기능을 일일히 작성하는 것이 아닌 하나로 모듈화할 수 있었다. 이를 통해 변경에 보다 쉽게 대처할 수 있게 되었다.

템플릿 메서드 패턴의 단점

템플릿 메서드 패턴은 상속을 사용하게 되어 부모 클래스와 자식 클래스가 컴파일 단계에서 강하게 결합되는 문제가 있다. 자식 클래스는 부모 클래스의 기능을 전혀 사용하지 않는다. 그러나 상속을 통해 강하게 의존되어 부모 클래스의 변경이 자식 클래스에 영향을 줄 수 있게 된다. 또한 핵심 로직마다 따로 클래스를 만들거나 익명 내부 클래스를 만들어서 처리를 해야하는 것은 번거롭다. 이를 개선하기 위한 방법으로 전략 패턴( Strategy Pattern)이 있는데 이는 다음 글에 설명하겠다.

이 내용은 인프런의 김영한님 강의 “스프링 핵심 원리 - 고급편”을 시청하고 정리한 글입니다.

댓글남기기