전략 패턴은 변하지 않는 부분을 Context
라는 곳에 두고, 변하는 부분을 Strategy
라는 인터페이스를 만들어 해당 인터페이스를 구현하여 문제를 해결한다.
템플릿 메서드 패턴과의 차이점은 상속이 아니라 위임으로 문제를 해결한다는 점이다.
변하지 않는 로직을 가진 템플릿 역할을 하는 코드이다.
컨텍스트는 변하지 않고 Strategy를 통해 전략이 수정된다고 생각하면 된다.
@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);
}
}
Strategy
의 구형체를 주입한다. Context
는 Strategy
인테페이스에만 의존한다는 것이다. Strategy
구현체는 Strategy
구현체에 영향을 받지 않는다. 전략을 인터페이스로 생성하여
public interface Strategy {
void call();
}
@Slf4j
public class StrategyLogic1 implements Strategy{
@Override
public void call() {
log.info("비즈니스 로직 1 실행");
}
}
@Test
void strategyV1() {
StrategyLogic1 strategyLogic1 = new StrategyLogic1();
ContextV1 context1 = new ContextV1(strategyLogic1);
context1.execute();
}
@Test
void strategyV1() {
// 익명 내부 클래스 활용
ContextV1 context1 = new ContextV1(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
});
context1.execute();
// 람다로 변환.
ContextV1 context2 = new ContextV1(() -> log.info("비즈니스 로직1 실행"));
context2.execute();
}
위의 예제를 보면 Context
에 Strategy
를 조립하고 Context
를 실행한다.
이는 스프링이 의존관계 주입을 통해 필요한 의존관계를 모두 맺고 실제 요청을 처리하는 것과 같은 원리이다.
이 방식의 단점은 Context
와 Strategy
를 조립한 후 변경하기 번거롭다는 점이다 . 물론 Context
에 setter
를 사용하여 변경할 수 있지만, 싱글톤으로 사용하는 경우 동시성 문제등이 있다.
@Slf4j
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);
}
}
execute(...)가 호출될 때 마다 항상 파라미터로 전략을 전달 받아 사용한다.
@Test
void strategyV1() {
ContextV2 context = new ContextV2();
context.execute(new StrategyLogic1());
context.execute(new StrategyLogic2());
}
/**
* 전략 패턴 익명 내부 클래스
*/
@Test
void strategyV2() {
ContextV2 context = new ContextV2();
context.execute(new Strategy() {
@Override
public void call() {
log.info("익명 내부 클래스로 로직 실행.");
}
});
context.execute(()->log.info("람다로 비즈니스 로직 실행"));
}
위의 ContextV2Test
예제에서 ContextV2
는 변하지 않는 템플릿 역할을 하고 , 변하는 부분은 파라미터로 넘어오는 Strategy
의 코드를 실행해서 처리한다.
이처럼 다른 코드의 인수로서 넘겨주는 실행가능한 코드를 콜백(callback)이라고 한다.
자바에서는 실행 가능한 코드를 인수로 넘기려면 객체가 필요하다. 자바8 이전에는 익명 내부 클래스를 사용했고, 자바8 이후에는 람다를 사용한다.
Context2
와 같은 방식의 전략패턴을 템플릿 콜백 패턴이라고 한다. Context
가 템플릿 역할을 하고, Strategy
부분이 콜백으로 넘어온다. JdbcTemplate
, RestTemplate
, TransactionTemplate
, RedisTemplate
처럼 XxxTemplate
가 있다면 템플릿 콜백 패턴으로 만들어진 것이라 생각하면 된다. public interface Callback {
void call();
}
@Slf4j
public class TimeLogTemplate {
public void execute(Callback callback) {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
callback.call(); // 위임
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
@Slf4j
public class TemplateCallbackTest {
@Test
void callbackV1(){
TimeLogTemplate template = new TimeLogTemplate();
template.execute(new Callback() {
@Override
public void call() {
log.info("비즈니스 로직 1 실행");
}
});
template.execute(()->log.info("비즌니스 로직 2 실행 "));
}
}
Strategy
역할의 Callback
인터페이스를 익명 내부 클래스 & 람다의 형태로 Template
에 전달하여 사용한다. 애플리케이션 개발할 때 템플릿 콜백 패턴과 같은 디자인 패턴을 사용하여 코드의 중복을 줄이는 등 장점이 있었다.
하지만 결국 부가 기능을 추가하기 위해 원본 코드를 수정해야 한다는 단점이 있다.
원본 코드를 수정하지 않고 부가기능을 추가하기 위해서는 프록시라는 개념을 이해할 필요가 있다.
스프링 핵심원리 고급편 - 김영한 https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard
스프링 입문을 위한 자바 객체지향의 원리와 이해