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 타입을 다루기 위한 반환 값
});
}