김영한의 스프링 핵심 원리(고급편) - 템플릿 메서드 패턴, 전략 패턴, 템플릿 콜백 패턴

2025. 2. 20. 01:07·김영한의 스프링 핵심 원리 - 고급편

Question

  • 템플릿 메서드 패턴의 장점은?
  • 템플릿 메서드 패턴의 단점은?
  • 전략 패턴은 템플릿 메서드 패턴의 단점을 어떻게 극복하는가?
  • 템플릿 콜백 패턴에 대해 설명하시오

 

중복 코드의 문제점

@GetMapping("/v3/request")
public String request(String itemId) {
    TraceStatus status = null;
    try {
        status = trace.begin("OrderController.request()");
        orderService.orderItem(itemId); // 핵심 기능
        trace.end(status);
    } catch (Exception e) {
        trace.exception(status, e);
        throw e;
    }
    return "ok";
}
  • 이전 글에서 다뤘던 것처럼 위 코드에서 핵심 기능은 orderService.orderItem(itemId); 임
  • 그 외에 나머지는 모두 계속 반복되는 패턴
  • 반복되는 패턴이라는 말은 이 코드에서 뿐만 아니라 여기 저기서 동일한 코드가 계속 사용된다는 말
  • 즉, 한 곳에서 코드 수정이 필요하면 다른 곳에서 모두 코드 수정을 해야 함.

 

핵심 기능 VS 부가 기능

  • 핵심 기능
    • 객체가 제공하는 고유의 기능
    • 예를 들어, 위의 예제에서는 orderService.orderItem이 핵심 기능
  • 부가 기능
    • 핵심 기능을 보조하기 위해 제공되는 기능
    • 위 코드에서는 orderService.orderItem 코드를 제외한 나머지가 부가 기능
    • 로깅, 트랜잭션 등이 보조 기능

 

템플릿 메서드 패턴

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



@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()");
    }
}
  • 변하는 것과 변하지 않는 부분을 분리
  • 위 예제에서 execute 함수 안에서 call()이 변하는 부분이고 나머지는 변하지 않는 부분
  • 따라서, call() 함수를 abstract 함수로 만들어서 자식 클래스가 이를 상속하도록 만듦
  • 변하지 않는 부분은 부모 클래스의 것을 그대로 사용하고 변하는 부분은 자식 클래스에서 오버라이딩 해서 사용하는 것
  • 따라서, 아래 Controller에서 AbstractTemplate 객체를 생성하면서 call() 함수를 오버라이딩 해 변하는 부분을 직접 넣어줌

 

템플릿 메서드 패턴이 좋은 이유?

  • 중복되는 코드를 줄여주기 때문에 좋은가?
  • 그 부분도 물론 좋지만, 결국 중요한 것은 코드의 변경이 일어날 때 기존에는 여기 저기 모두 변경해야 했다면, 탬플릿 메서드 패턴이 적용된 이후에는 한 곳만 변경을 하면 나머지도 모두 적용이 됨
  • 이는 좋은 설계의 기본 원칙
  • 이러한 원칙을 단일 책임 원칙이라고 함(Single Responsibility Principle)
    • 하나의 클래스는 하나의 역할만 수행해야 한다
    • 클래스가 하나의 역할만 담당하므로 변경 시에 그 클래스만 수정하면 된다

 

템플릿 메서드 패턴의 단점

  • 템플릿 메서드 패턴의 단점은 상속을 사용한다는 것
  • 상속을 사용한다는 것은 자식이 부모에 대해서 엄청난 의존성이 있다는 것
  • 즉, 부모의 변경의 자식에게 직접적으로 영향을 끼친다는 것
  • 공통된 부분을 부모에, 변경되는 부분을 자식에 넣어서 사용하기 대문에 부모의 변경은 공통의 변경이긴 함
  • 하지만, 이는 개발시 부모의 변경이 모든 공통의 변경을 의도하지 않을 수도 있음
  • 즉, 자식이 부모에 대한 엄청난 의존성을 갖는 것은 부모가 변경될 시에 예상치 못한 변경점으로 문제가 생길수 있다는 점
  • 이는 OCP에서 변경에 대해 닫혀 있어야 한다는 원칙을 어긋나게 됨

 

 

전략 패턴

public interface Strategy {
	void call();
}

@Slf4j
public class StrategyLogic1 implements Strategy {
	@Override
	public void call() {
		log.info("비즈니스 로직1 실행");
	}

}


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


@Test
void strategyV1() {
	Strategy strategyLogic1 = new StrategyLogic1();
	ContextV1 context1 = new ContextV1(strategyLogic1);
	context1.execute();
	
	Strategy strategyLogic2 = new StrategyLogic2();
	ContextV1 context2 = new ContextV1(strategyLogic2);
	context2.execute();
}
  • 전략 패턴은 템플릿 메세드 패턴의 단점을 개선함
  • 템플릿 메서드 패턴은 상속의 단점을 가지고 있었고, 전략 패턴은 이를 상속이 아닌 구현의 개념으로 변경
  • 그러면, 구현체 입장에서는 인터페이스의 변화를 걱정할 필요가 없음
  • 기존에 공통된 로직을 부모 클래스에서 가지고 있던 걸 별도의 클래스로 변경
  • 위의 예제는 조립을 하고 실행을 하는 예제
  • 조립을 하고 실행하는 예제는 조립한 이후에 여러 번 실행할 때 좋음
  • 만약에 실행할 때 마다 다른 함수를 조립해야 한다면, 실행할 때 조립하는 방법도 좋음
  • 예를 들어서 아래의 코드
    ContextV2 context = new ContextV2();
    context.execute(() -> log.info("비즈니스 로직1 실행"));
    context.execute(() -> log.info("비즈니스 로직2 실행"));

 

템플릿 콜백 패턴

public class TraceTemplate {
    private final LogTrace trace;

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

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


@RestController
public class OrderControllerV5 {
    private final OrderServiceV5 orderService;
    private final TraceTemplate template;

    public OrderControllerV5(OrderServiceV5 orderService, LogTrace trace) {
        this.orderService = orderService;
        this.template = new TraceTemplate(trace);
    }

    @GetMapping("/v5/request")
    public String request(String itemId) {
        return template.execute("OrderController.request()", new TraceCallback<>() {
            @Override
            public String call() {
                orderService.orderItem(itemId);
                return "ok";
            }
        });
    }
}
  • 템플릿 콜백 패턴은 사실상 전략 패턴과 거의 유사한데, 파라미터 자체로 함수를 받음
  • 함수를 바로 받을 수 있다 보니 인터페이스나 상속을 사용 없이 훨씬 간단하게 사용 가능
  • 자바 8부터는 람바식 사용 가능하며, 자바 8 이전에는 익명 클래스를 활용
  • 위는 익명 클래스의 예제
  • 아래는 람다 식을 활용한 코드
    public void orderItem(String itemId) {
        template.execute("OrderService.orderItem()", () -> {
            orderRepository.save(itemId);
            return null; // Void 타입을 다루기 위한 반환 값
        });
    }

'김영한의 스프링 핵심 원리 - 고급편' 카테고리의 다른 글

김영한의 스프링 핵심 원리(고급편) - 빈 후처리기  (0) 2025.02.22
김영한의 스프링 핵심 원리(고급편) - 프록시 팩토리  (0) 2025.02.22
김영한의 스프링 핵심 원리(고급편) - 동적 프록시 기술  (0) 2025.02.21
김영한의 스프링 핵심 원리(고급편) - 프록시 패턴, 데코레이터 패턴  (0) 2025.02.21
김영한의 스프링 핵심 원리(고급편) - (Log Trace, ThreadLocal)  (0) 2025.02.18
'김영한의 스프링 핵심 원리 - 고급편' 카테고리의 다른 글
  • 김영한의 스프링 핵심 원리(고급편) - 프록시 팩토리
  • 김영한의 스프링 핵심 원리(고급편) - 동적 프록시 기술
  • 김영한의 스프링 핵심 원리(고급편) - 프록시 패턴, 데코레이터 패턴
  • 김영한의 스프링 핵심 원리(고급편) - (Log Trace, ThreadLocal)
5jyan5
5jyan5
  • 5jyan5
    jyan
    5jyan5
  • 전체
    오늘
    어제
    • 분류 전체보기 (242)
      • 김영한의 스프링 핵심 원리(기본편) (8)
      • 김영한의 스프링 핵심 원리 - 고급편 (11)
      • 김영한의 스프링 MVC 1편 (1)
      • 김영한의 스프링 DB 1편 (3)
      • 김영한의 스프링 MVC 2편 (3)
      • 김영한의 ORM 표준 JPA 프로그래밍(기본편) (9)
      • 김영한의 스프링 부트와 JPA 활용2 (2)
      • 김영한의 실전 자바 - 중급 1편 (1)
      • 김영한의 실전 자바 - 고급 1편 (9)
      • 김영한의 실전 자바 - 고급 2편 (9)
      • Readable Code: 읽기 좋은 코드를 작성.. (2)
      • 김영한의 실전 자바 - 고급 3편 (9)
      • CKA (118)
      • 개발 (37)
      • 경제 (4)
      • 리뷰 (1)
      • 정보 (2)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      Target
      JPQL
      jdk 동적 프록시
      cglib
      프록시
      reentarantlock
      빈 후처리기
      log trace
      버퍼
      고급
      @args
      typequery
      gesingleresult
      hibernate5module
      Thread
      requset scope
      jpq
      WAS
      프록시 팩토리
      조회 성능 최적화
      @discriminatorvalue
      @discriminatorcolumn
      스레드
      @within
      페치 조인
      김영한
      단방향 맵핑
      자바
      락
      양방향 맵핑
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.2
    5jyan5
    김영한의 스프링 핵심 원리(고급편) - 템플릿 메서드 패턴, 전략 패턴, 템플릿 콜백 패턴
    상단으로

    티스토리툴바