전략 패턴과 템플릿 콜백 패턴
업데이트:
전략 패턴(Strategy Pattern)
- 전략 패턴은 컨텍스트(context)에서 사용할 변경 가능한 전략(Strategy)을 인터페이스를 이용하여 캡슐화하고, 필요에 따라 동적으로 변경하여 사용할 수 있도록 하는 디자인 패턴이다.
테스트를 통한 패턴 학습
지난 글인 탬플릿 메서드 패턴에서 사용한 테스트 로직과 동일한 코드를 전략패턴을 통해 풀어보려고 한다. 전략 패턴은 변하지 않는 부분을 Context
에 두고 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);
}
}
public interface Strategy {
void call();
}
틀이 되는 Context
코드와 Strategy
인터페이스다. 생성자에서 strategy를 받아 필드로 초기화해서 call() 메서드를 호출한다. 아래는 구조에 대한 그림이다.
@Slf4j
public class StrategyLogic1 implements Strategy {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
}
@Slf4j
public class StrategyLogic2 implements Strategy {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
}
GOF 디자인 패턴에서 정의한 전략 패턴의 의도는 다음과 같다.
- 알고리즘 제품군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만들자. 전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.
전략 패턴의 핵심은 Context
는 Strategy
인터페이스만 의존한다는 점이다. 덕분에 Strategy
의 구현체를 변경하거나 새로 만들어도 Context
는 영향을 받지 않는다.
스프링에서 의존관계 주입에서 사용하는 방식이 바로 전략 패턴이다.
@Slf4j
public class ContextV1Test {
@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();
}
}
ContextV1에 Strategy의 구현체인 StrategyLogic1, StrategyLogic2를 주입하여 context.execute()를 통해 원하는 부분만 자유롭게 변경했다.
전략 패턴은 익명 내부 클래스 또는 람다로 변경할 수 있다. 람다로 변경하기 위해선 인터페이스에 추상 메서드가 1개만 있으면 되는데(함수형 인터페이스라고 한다.) Strategy
인터페이스는 메서드가 1개 뿐이므로 람다로 변경해보겠다.
@Slf4j
public class ContextV1Test {
ContextV1 context1 = new ContextV1(() -> log.info("비즈니스 로직1 실행"));
context1.execute();
ContextV1 context2 = new ContextV1(() -> log.info("비즈니스 로직2 실행"));
context2.execute();
}
현재 방식은 Context
내부 필드에 Strategy
를 두고 사용한다. 이 방식은 실행 전에 원하는 모양으로 조립하고, 그 다음에 Context
를 실행하는 선 조립, 후 실행 방식에 매우 유용하다. 조립하고 난 후에는 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);
}
}
ContextV2
는 전략을 필드로 가지고 있지 않는다. 메서드에 파라미터로 넘기는 방식을 사용했다.
@Slf4j
public class ContextV2Test {
@Test
void strategyV3() {
ContextV2 context = new ContextV2();
context.execute(() -> log.info("비즈니스 로직1 실행"));
context.execute(() -> log.info("비즈니스 로직2 실행"));
}
}
Context
와 Strategy
를 ‘선 조립 후 실행’하는 방식이 아니라 Context
를 실행할 때 마다 전략을 인수로 전달한다.
클라이언트는 Context
를 실행하는 시점에 원하는 Strategy
를 전달할 수 있다. 따라서 이전 방식과 비교해서 원하는 전략을 더욱 유연하게 변경할 수 있다.
테스트 코드를 보면 하나의 Context
만 생성한다. 그리고 하나의 Context
에 실행 시점에 여러 전략을 인수로 전달해서 유연하게 실행하는 것을 확인할 수 있다.
템플릿
- 변하지 않는 부분을 템플릿이라 하고 변하는 부분에 다른 코드 조각을 넘겨서 실행하게 되었다.
ContextV2
는 변하지 않는 템플릿 역할을 한다. 그리고 변하는 부분은 파라미터로 넘어온Strategy
의 코드를 실행해서 처리한다. 이렇게 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를콜백(callback)
이라 한다.
콜백이란?
- 프로그래밍에서 콜백(callback) 또는 콜애프터(call-after function)는 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 뜻한다. 콜백을 넘겨받는 코드는 필요에 따라 즉시 실행할 수도 있고, 아니면 나중에 실행할 수도 있다.
자바 언어에서 콜백
- 자바8 부터 람다를 사용해 인수로 넘긴다.
- 자바8 이전에는 하나의 메소드를 가진 인터페이스를 구현 후, 익명 내부 클래스를 사용했다.
템플릿 콜백 패턴
- 위 코드에서
ContextV2
가 템플릿 역할을 하고,Strategy
부분이 콜백으로 넘어온다고 생각하면 된다. - 전략 패턴에서 템플릿과 콜백 부분이 강조된 패턴이라고 생각하면 된다.
JpdbcTemplate
,RestTemplate
,TransactionTemplate
,RedisTemplate
같은 다양한 템플릿 콜백 패턴이 사용된다.
템플릿 콜백 패턴의 한계
템플릿 콜백 패턴을 적용해서 변하는 코드와 변하지 않는 코드를 분리해서 최적화를 했다. 하지만 이를 운영 코드에 적용하기 위해서는 원본 코드를 수정 해야만 한다. 클래스가 수백개면 수백개를 다 수정해야하는 것이다.
원본 코드를 손대지 않고 부가 로직을 적용하기 위해서 다음 글은 프록시에 대해서 작성할 것이다.
이 내용은 인프런의 김영한님 강의 “스프링 핵심 원리 - 고급편”을 시청하고 정리한 글입니다.
댓글남기기