템플릿 메서드 Template Method 패턴과 차이
Template Method 패턴에서는 자주 변경되는 점과 변경되지 않는 점을 부모와 자식 관계인 '상속 관계'로 해결했다. 하지만 단점으로 의존성이 높았다.
Strategy 전략 패턴
전략패턴에서는 상속 관계를 '위임 관계' 로 변경함으로써 의존성을 낮출 수 있다.
위임 관계는 관계 있는 것들을 조립하여 실행하는 방법이다. 조립에는 생성할 때 조립하는 방법이 있고 호출할 때 조립하는 방식이 있다.
public class ContextV1 {
private final Strategy strategy;
public ContextV1(Strategy strategy) {
this.strategy = strategy;
}
public void execute(){
strategy.callLogic();
}
}
public class ContextV2 {
public void execute(Strategy strategy){
strategy.callLogic();
}
}
public interface Strategy {
void callLogic();
}
ContextV1은 조립하고 실행하는 방식이고, ContextV2는 실행 시 조립하는 방법이다. V2 방법이 좀 더 편리하다.
public class ContextTest {
@Test
void testV1(){
ContextV1 contextV1 = new ContextV1(new Strategy() {
@Override
public void callLogic() {
System.out.println("로직 1실행");
}
});
contextV1.execute();
}
@Test
void testV2(){
ContextV2 context = new ContextV2();
context.execute(()-> System.out.println("로직1 실행"));
}
}
템플릿 콜백 Template Callback 패턴
전략패턴을 활용한 패턴인데, Spring 프레임워크에서 많이 사용하는 패턴이다. 스프링에서는 JdbcTemplate, RestTemplate 등이 있다.
public class TimeLogTemplate {
private final LogTrace trace;
public TimeLogTemplate(LogTrace logTrace) {
this.trace = logTrace;
}
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;
}
}
}
public interface LogTrace {
TraceStatus begin(String message);
void end(TraceStatus status);
void exception(TraceStatus status, Exception e);
}
@RestController
public class OrderControllerV3 {
private final OrderServiceV3 orderService;
private final TimeLogTemplate timeLogTemplate;
public OrderControllerV3(OrderServiceV3 orderService, LogTrace logTrace){
this.orderService = orderService;
this.timeLogTemplate = new TimeLogTemplate(logTrace);
}
@GetMapping("/v3/request")
public String request(String itemId) {
return timeLogTemplate.execute("OrderController.request()", ()->{
orderService.orderItem(itemId); return "OK";
});
}
}
@Service
@RequiredArgsConstructor
public class OrderServiceV3 {
private final OrderRepositoryV3 orderRepositoryV2;
private final TimeLogTemplate timeLogTemplate;
public void orderItem(String itemId){
timeLogTemplate.execute("OrderService.request()", ()->{
orderRepositoryV2.save(itemId);
return null;
});
}
}
OrderControllerV3 에서는 Bean으로 바로 TimeLogTemplate을 주입 받는 것이 아니라, 생성자에서 LogTrace를 파라미터로 받아서 TimeLogTemplate를 생성한다.
파라미터로 Template을 바로 받는 방식과 생성자 내에서 생성하는 방식 2가지는 크게 차이가 없다. 생성자 내에서 template 생성하는 방식은 테스트 코드를 작성할 때 TimeLogTemplate을 생성하지 않아도 되는 편리함이 있을 뿐이다.
@Repository
@RequiredArgsConstructor
public class OrderRepositoryV3 {
private final TimeLogTemplate timeLogTemplate;
public void save(String itemId) {
timeLogTemplate.execute("OrderRepository.save()", () -> {
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
sleep(1000);
return null;
});
}
private void sleep(int milliseconds) {
try {
Thread.sleep(milliseconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class OrderV3Test {
private OrderServiceV3 orderService;
private ThreadLocalLogTrace logTrace;
@BeforeEach
void setup() {
this.logTrace = new ThreadLocalLogTrace();
TimeLogTemplate timeLogTemplate = new TimeLogTemplate(logTrace);
OrderRepositoryV3 orderRepository = new OrderRepositoryV3(timeLogTemplate);
this.orderService = new OrderServiceV3(orderRepository, timeLogTemplate);
}
@Test
void test1() {
// given
OrderControllerV3 controllerV3 = new OrderControllerV3(orderService, logTrace);
// when
String result = controllerV3.request("pizza1");
// then
assertThat(result).isEqualTo("OK");
}
}
03:27:36.473 [Test worker] INFO hello.advanced.trace.logtrace.ThreadLocalLogTrace - [d329474d] OrderController.request()
03:27:36.481 [Test worker] INFO hello.advanced.trace.logtrace.ThreadLocalLogTrace - [d329474d] |-->OrderService.request()
03:27:36.481 [Test worker] INFO hello.advanced.trace.logtrace.ThreadLocalLogTrace - [d329474d] | |-->OrderRepository.save()
03:27:37.495 [Test worker] INFO hello.advanced.trace.logtrace.ThreadLocalLogTrace - [d329474d] | |<--OrderRepository.save() time=1014ms
03:27:37.495 [Test worker] INFO hello.advanced.trace.logtrace.ThreadLocalLogTrace - [d329474d] |<--OrderService.request() time=1018ms
03:27:37.495 [Test worker] INFO hello.advanced.trace.logtrace.ThreadLocalLogTrace - [d329474d] OrderController.request() time=1023ms
디자인 패턴 의도
디자인 패턴을 사용할 때는 이렇게 상황에 맞게 변경해서 사용해도 좋다. 지켜할 점은 이 디자인 패턴의 '의도'이다.
GOF 전략 패턴의 의도는 다음과 같다.
알고리즘 제품군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만들자. 전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.
전략패턴에서 '선 조립 후 실행'과 '실행 시 조립' 은 이 의도를 지켰다고 볼 수 있다.
정리
Template Method 의 상속관계 대신 위임 관계 적용으로 의존관계를 줄였다. 변하지 않는 로깅 부분과 자주 변할 가능성이 높은 비지니스 로직을 좀 더 분리시켰다.
하지만 비지니스 로직 내에 로깅을 위한 코드가 들어감으로써 의존 관계가 여전히 있다. 프록시 패턴을 통해 개선해볼 수 있다.
'공부노트 > 스프링' 카테고리의 다른 글
Spring AOP 적용 (0) | 2022.06.25 |
---|---|
스프링 AOP (0) | 2022.06.19 |
프록시 패턴과 데코레이션 패턴 (0) | 2022.04.30 |
Template Method 패턴 (0) | 2022.04.24 |
스프링 프레임워크 - 기초 (0) | 2022.04.10 |