오봉이와 함께하는 개발 블로그

스프링 핵심 원리 - 고급편 > 템플릿 메서드 패턴 (적용 1, 적용 2, 정의) 본문

BE/Spring

스프링 핵심 원리 - 고급편 > 템플릿 메서드 패턴 (적용 1, 적용 2, 정의)

오봉봉이 2024. 5. 10. 00:17
728x90

템플릿 메서드 패턴 - 적용 1

AbstractTemplate.java

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() 은 변하는 부분을 처리하는 메서드이다. 이 부분은 상속으로 구현해야 한다.

OrderControllerV4

    @GetMapping("/v4/request")
    public String request(@RequestParam("itemId") String itemId) {
        AbstractTemplate<String> template = new AbstractTemplate<>(trace) {
            @Override
            protected String call() {
                orderService.orderItem(itemId);
                return "ok";
            }
        };
        return template.execute("OrderController.request()");
    }
  • AbstractTemplate\<String\>
    • GenericString으로 설정
    • 따라서 AbsctractTemplate의 반환 타입은 String이 된다.
  • 익명 내부 클래스
    • 익명 내부 클래스를 사용한다.
    • 객체를 생성하며 AbsctractTemplate을 상속받은 자식 클래스를 정의
      • 별도의 클래스를 생성하지 않아도 된다.
  • template.execute("OrderController.request()")
    • 템플릿을 실행하며 로그로 남길 message 전달

OrderServiceV4

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

        template.execute("OrderService.orderItem()");
    }
  • AbstractTemplate<Void>
    • Generic을 사용하여 반환 타입이 필요한데, 반환할 내용이 없다면, Void 타입(클래스)을 사용하고 null을 반환하면 된다.

OrderRepositoryV4

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

실행하면 잘 찍히는 것을 확인할 수 있다.

[3bb813c5] OrderController.request()
[3bb813c5] |-->OrderService.orderItem()
[3bb813c5] |   |-->OrderRepository.save()
[3bb813c5] |   |<--OrderRepository.save() time=1004ms
[3bb813c5] |<--OrderService.orderItem() time=1005ms
[3bb813c5] OrderController.request() time=1006ms

템플릿 메서드 패턴 - 적용 2

템플릿 메서드 패턴을 사용하여 변하는 코드와 변하지 않는 코드를 분리했다..
로그를 출력하는 템플릿 역할을 하는 변하지 않는 코드는 모두 AbstractTemplate에 담아두고, 변하는 코드는 자식 클래스를 만들어 분리했다.

//OrderServiceV0 코드
public void orderItem(String itemId) {
     orderRepository.save(itemId);
 }

//OrderServiceV3 코드
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;
    } 
}

//OrderServiceV4 코드
AbstractTemplate<Void> template = new AbstractTemplate<>(trace) {
     @Override
     protected Void call() {
         orderRepository.save(itemId);
         return null;
     }
};
template.execute("OrderService.orderItem()");

좋은 설계

좋은 설계에 대한 정의는 수도 없이 많겠지만, 진가가 드러날 때는 변경이 일어날 때다.
로그를 남기는 부분을 모두 모아 하나로 모듈화 하고, 비즈니스 로직을 분리하였다.
만약 로그 남기는 코드가 분리되지 않아 여러 곳에 있을 때 로그 코드에 변경이 발생하면 모든 곳을 찾아서 변경해야 한다.
하지만 AbstractTemplate에 로그 코드를 모아두었기 때문에 로그 기능이 변경되었을 때 AbstractTemplate만 수정하면 된다.

템플릿 메서드 패턴 - 정의

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

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

GOF 템플릿 메서드 패턴 정의

image

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

문제

템플릿 메서드 패턴은 추상 클래스르 사용하여 상속을 사용한다. 따라서 상속의 문제를 그대로 안고가야 하는 단점이 존재한다.
자식 클래스가 부모 클래스에 너무 의존하게 된다. 한 클래스가 다른 클래스에 의존할 때는 해당 클래스 수정 시 높은 확률로 다른 클래스도 수정이 되어야 하고, 거기에 엮인 모든 클래스에 수정이 발생할 수도 있다.
그렇게 되면 특정 클래스의 특정 부분만 변경하고 싶은데, 엮인 모든 클래스가 변경되어 어디서 어떤 사이드 이펙트가 발생할지 모르는 상황이 된다.
또한 이전 예제들에서는 자식 클래스 입장에서는 부모 클래스의 기능을 사용하지 않는데 상속 받고 있다.

상속을 받는다는 것은 특정 부모 클래스를 의존하고 있다는 것이다.
자식 클래스의 extends 다음에 바로 부모 클래스가 코드상에 지정되어 있다.
따라서 부모 클래스의 기능을 사용하든 사용하지 않든 간에 부모 클래스를 강하게 의존하게 된다.
여기서 강하게 의존한다는 뜻은 자식 클래스의 코드에 부모 클래스의 코드가 명확하게 적혀 있다는 뜻이다.
UML에서 상속을 받으면 삼각형 화살표가 자식 -> 부모를 향하고 있는 것은 이런 의존관계를 반영하는 것이다.

추가로 템플릿 메서드 패턴은 상속 구조를 사용하기 때문에, 별도의 클래스나 익명 내부 클래스를 만들어야 하는 부분도 복잡하다.

 

출처: 김영한 지식공유자님의 스프링 핵심 원리 고급편

728x90
Comments