[Spring] : 템플릿 메서드 패턴과 콜백 패턴

Loopy·2023년 1월 4일
0

스프링

목록 보기
5/16
post-thumbnail

☁️ 배경

현재 상태의 로그 추적기에는 문제가 있다. 어떤 것일까?


  TraceStatus status = null;
  try {
      status = trace.begin("message"); 
      //핵심 기능 호출하는 부분
      trace.end(status);
  } catch (Exception e) {
      trace.exception(status, e);
      throw e; 
  }

바로 위와 같은 패턴이 Controller, Service, Repository 모두에 반복되고 있다는 것이다. 만약 클래스가 수백 개라면? 코드의 중복이 너무 심해질 것이다.

따라서, 우리는 핵심 기능과 부가 기능을 분리해야 한다.

🔖 핵심 기능 vs 부가 기능
1) 핵심 기능 : 해당 객체가 제공하는 고유의 기능
2) 부가 기능: 핵심 기능을 보조하기 위해 제공되는 기능. 예를 들어서 로그 추적 로직, 트랜잭션 기능이 존재

좋은 설계란 변하는 것과 변하지 않는 것을 분리하여, 모듈화 하는 것이다.
즉 현재 상황에서 핵심 기능은 변하는 부분, 로그를 출력하기 위한 부가 기능은 변하지 않는 부분이 되므로 변하지 않는 부분을 따로 빼내서 공통화 시키면 된다.

바로 이러한 전략을 템플릿 메서드 패턴이라 한다.

☁️ 템플릿 메서드 패턴

템플릿 메서드 패턴은 부모 클래스에 변하지 않는 템플릿 코드를 둔다. 그리고 변하는 부분은, 자식 클래스에 두고 상속과 오버라이딩을 사용해서 처리한다.

  • AbstractTemplate.execute() : 변하지 않는 템플릿 코드
  • AbstractTemplate.call() : 변하는 부분

템플릿 메서드 패턴 장점

단순히 코드 중복을 제거해준다는 것에서 끝나지 않는다.

가장 중요한 장점은, 부가 기능의 변경이 일어났을때 AbstractTemplate 하나만 고치면 되므로 단일 책임 원칙(SRP)을 잘 지키면서 구조화가 가능해진다는 것이다.

템플릿 메서드 패턴 적용

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();  // 변경되는 부분은 자식 클래스에 위임
}
public class 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()");
    }
 }

🔖 익명 내부 클래스
객체를 생성하면서 부모 클래스를 상속받은 자식 클래스를 정의하는 방법이다.
따라서 별도의 자식 클래스를 직접 만들지 않아도 되며, 이름이 존재하지 않으므로 스프링에서 임의로 이름을 붙여준다.

템플릿 메서드 패턴의 단점

하지만 템플릿 메서드 패턴은 상속을 사용하는 것 자체가 단점이 된다.

상속을 한다는 것은, 부모 클래스와 자식 클래스가 강하게 결합되어 의존하고 있다는 것을 말한다. 하지만 현재 자식 클래스는 부모 클래스의 기능을 하나도 사용하지 않고 있음에도 상속을 통해 부모 클래스의 모든 코드를 알게 되는 것이다.

부모 클래스에서 변경이 일어나면 자식까지 영향이 전파되기 때문에 좋지 않은 설계가 만들어진다.

이러한 문제를 해결하기 위해 나온, 템플릿 메서드 패턴과 유사한 역할을 하면서 상속의 단점을 제거해주는 패턴인 전략 패턴에 대해 알아보자.

☁️ 전략 패턴

전략 패턴이란, 상속이 아닌 인터페이스의 위임을 사용하여 변하는 부분을 캡슐화 하는 방식을 의미한다.

  • Context : 변하지 않는 로직을 가지고 있는 문맥
  • Strategy : 변하는 알고리즘을 캡슐화

Context는 내부에 필드로 Strategy를 가지고 있으므로, 전략이 바뀐다면 맞추어서 주입만 해주면 되는 장점이 존재한다. (ex) StrategyLogic1 -> StrategyLogic2)

작동 방식

  1. Context 에 원하는 Strategy 구현체를 주입한다.
  2. 클라이언트는 context 를 실행한다.
  3. contextcontext 로직을 시작한다.
  4. context 로직 중간에 strategy.call() 을 호출해서 주입 받은 strategy 로직을 실행한다.

전략 패턴 장점

템플릿 메서드 패턴과 다르게 구현체들은 Strategy 인터페이스에만 의존하고 있기 때문에, 변하지 않는 부분인 Context 가 변경되어도 전혀 영향을 받지 않아 안정적인 상태가 된다.

또한, ContextStrategy를 실행 전에 원하는 모양으로 조립해두고 그 다음에 실행만 하면 되는 선 조립, 후 실행 방식이다.

스프링에서 애플리케이션을 개발할 때, 로딩 시점에 의존관계 주입을 통해 필요한 의존관계를 모두 맺어두고 난 다음 실제 요청을 처리하는 것과 같은 원리이다!

전략 패턴 단점

일단 조립한 이후에는, 전략을 변경하기가 번거롭다는 점이다. 물론 Contextsetter 를 제공해서 Strategy 를 넘겨 받아 변경하면 되지만, Context 를 싱글톤으로 사용할 때는 동시성 이슈 등 고려할 점이 많다.

전략 패턴 적용

  1. 필드에 전략을 보관
public class ContextV1 {

    private final Strategy strategy;

    public ContextV1(Strategy strategy) {
        this.strategy = strategy;
    }

    public void execute() {
        long startTime = System.currentTimeMillis();
        //비즈니스 로직 실행
        strategy.call();   // 위임
        //비즈니스 로직 종료
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime={}", resultTime);
    }
}
  1. 전략을 파라미터로 전달
public class ContextV2 {

    public void execute(Strategy strategy) {
        long startTime = System.currentTimeMillis();
        //비즈니스 로직 실행
        strategy.call();   // 위임
        //비즈니스 로직 종료
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime={}", resultTime);
    }
}
   @Test
    void strategyV2() {
        ContextV2 contextV2 = new ContextV2();
        contextV2.execute(() -> log.info("비즈니스 로직1 실행"));
        contextV2.execute(() -> log.info("비즈니스 로직 2 실행"));
    }

전략을 직접 파라미터로 전달하는 방식은, 필드로 가지고 있는 방식보다 실행 시점에 전략을 바꿀 수 있어 유연성이 증가한다는 장점이 존재한다. 하지만 단점 역시 실행할 때 마다 전략을 계속 지정해주어야 한다.

변하지 않는 템플릿 안에서 변하는 부분의 작은 코드 조각을 넘겨서 실행하는 것이 목적이기 때문에, 이러한 부분에서 보면 실행 시점에 유연하게 코드 조각을 전달하는 2번 방식이 더 적합해 보인다.

☁️ 템플릿 콜백 패턴

템플릿 콜백 패턴이란, 위에서 언급한 전략을 파라미터로 전달하는 방식과 같다.

🔖 콜백(call back)이란?
다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 의미한다. 콜백을 넘겨받는 코드는 이 콜백을 필요에 따라 즉시 실행할 수도 있고, 아니면 나중에 실행할 수도 있다.

즉 코드 호출( call )이 클라이언트가 아닌 인수로 넘겨주는 뒤( back )에서 실행되는 것을 말한다. 자바에서는 실행 가능한 코드를 인수로 넘기려면 객체가 필요한데, 지비 8 이후부터는 익명 클래스가 아닌 람다를 사용해 간단하게 표현할 수 있다.

참고로 템플릿 콜백 패턴은 GOF 패턴은 아니지만, 스프링에서 자주 사용한다.
예를 들어 JdbcTemplate, RedisTemplate, RestTemplate, TransactionTemplate 과 같이 xxxTemplate 은 모두 템플릿 콜백 패턴으로 이루어져 있다.

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글