스프링 핵심 원리(고급편)

Ch03. 템플릿 메서드 패턴과 콜백 패턴 - 템플릿 메서드 패턴(적용)

webmaster 2022. 4. 9. 23:05
728x90

템플릿 메서드 패턴 적용

public abstract class AbstractTemplate<T> {

    private final LogTrace trace;

    public AbstractTemplate(LogTrace trace) {
        this.trace = trace;
    }

    public T execute(String message){
        TraceStatus status = null;
        try{
            status = trace.begin(message);
            //로직 호출
            T result = call();
            trace.end(status);
            return result;
        }catch (Exception e){
            trace.exception(status, e);
            throw e;
        }
    }

    protected abstract T call();
}
  • AbstractTemplate 은 템플릿 메서드 패턴에서 부모 클래스이고, 템플릿 역할을 한다.
  • <T> 제네릭을 사용했다. 반환 타입을 정의한다.
  • 객체를 생성할 때 내부에서 사용할 LogTrace trace를 전달받는다.
  • 로그에 출력할 message를 외부에서 파라미터로 전달받는다.
  • 템플릿 코드 중간에 call() 메서드를 통해서 변하는 부분을 처리한다.
  • abstract T call() 은 변하는 부분을 처리하는 메서드이다. 이 부분은 상속으로 구현해야 한다

controller

@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

@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

@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();
        }
    }
}
  • AbstractTemplate<String>
    • 제네릭을 String으로 설정했다. 따라서 AbstractTemplate의 반환 타입은 String 이 된다.
  • 익명 내부 클래스
    • 익명 내부 클래스를 사용한다. 객체를 생성하면서 AbstractTemplate를 상속받은 자식 클래스를 정의했다.
    • 따라서 별도의 자식 클래스를 직접 만들지 않아도 된다.
  • template.execute("OrderController.request()")
    • 템플릿을 실행하면서 로그로 남길 message를 전달한다.
  • AbstractTemplate <Void>
    • 제네릭에서 반환 타입이 필요한데, 반환할 내용이 없으면 Void 타입을 사용하고 null을 반환하면 된다. 참고로 제네릭은 기본 타입인 void , int 등을 선언할 수 없다

좋은 설계

  • 좋은 설계라는 것은 무엇일까? 수많은 멋진 정의가 있겠지만, 진정한 좋은 설계는 바로 변경이 일어날 때 자연스럽게 드러난다.
  • 지금까지 로그를 남기는 부분을 모아서 하나로 모듈화 하고, 비즈니스 로직 부분을 분리했다. 여기서 만약 로그를 남기는 로직을 변경해야 한다고 생각해보자. 그래서 AbstractTemplate 코드를 변경해야 한다 가정해보자. 단순히 AbstractTemplate 코드만 변경하면 된다.
  • 템플릿이 없는 V3 상태에서 로그를 남기는 로직을 변경해야 한다고 생각해보자. 이 경우 모든 클래스를 다 찾아서 고쳐야 한다. 클래스가 수백 개라면 생각만 해도 끔찍하다
  • 단일 책임 원칙(SRP)
    • V4는 단순히 템플릿 메서드 패턴을 적용해서 소스코드 몇 줄을 줄인 것이 전부가 아니다.
    • 로그를 남기는 부분에 단일 책임 원칙(SRP)을 지킨 것이다. 변경 지점을 하나로 모아서 변경에 쉽게 대처할 수 있는 구조를 만든 것이다.

템플릿 메서드 패턴 정의

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

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

GOF 디자인 패턴의 템플릿 메소드 패턴 정의

  • 부모 클래스에 알고리즘의 골격인 템플릿을 정의하고, 일부 변경되는 로직은 자식 클래스에 정의하는 것이다.
  • 이렇게 하면 자식 클래스가 알고리즘의 전체 구조를 변경하지 않고, 특정 부분만 재정의할 수 있다.
  • 결국 상속과 오버 라이딩을 통한 다형성으로 문제를 해결하는 것이다.
  • 단, 템플릿 메서드 패턴은 상속을 사용한다. 따라서 상속에서 오는 단점들을 그대로 안고 간다
    • 특히 자식 클래스가 부모 클래스와 컴파일 시점에 강하게 결합되는 문제가 있다. 이것은 의존관계에 대한 문제이다. 자식 클래스 입장에서는 부모 클래스의 기능을 전혀 사용하지 않는다.
    • 부모 클래스에 메서드가 추가될 경우 자식 클래스는 부모 클래스를 의존하고 있기에 모든 코드를 수정해야 한다.
      • 부모 클래스의 기능을 사용하든 사용하지 않든 간에 부모 클래스를 강하게 의존하게 된다
      • 강하게 의존한다는 뜻은 자식 클래스의 코드에 부모 클래스의 코드가 명확하게 적혀 있다는 뜻이다
      • UML에서 상속을 받으면 삼각형 화살표가 자식 -> 부모를 향하고 있는 것은 이런 의존관계를 반영하는 것이다.
    • 추가로 템플릿 메서드 패턴은 상속 구조를 사용하기 때문에, 별도의 클래스나 익명 내부 클래스를 만들어야 하는 부분도 복잡하다.

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

728x90