템플릿 메서드 패턴은 부모 추상 클래스가 변경되면 그 하위 구현체인 자식 클래스들까지 영향을 받는다는 단점이 있다.
즉, 자식 클래스들이 부모 추상 클래스에 의존하고 있다는 점이 문제이다.
이러한 단점을 보완한 것이 바로 전략 패턴이다.
📝 전략 패턴의 기본 구조
아래 첫 번째 그림처럼 Context
클래스의 execute()
메서드에 변하지 않는 로직 부분을 구현하고, 변하는 부분은 Strategy
라는 인터페이스의 call()
메서드를 통해 분리한다. 이로써 상속이 아닌 위임(composition)으로 문제를 해결한다.
이는 스프링의 의존성 관리 패턴과 동일하다.
전략 패턴 동작 흐름
아래 그림은 전략 패턴이 동작하는 전체 흐름을 시각화한 것이다.
- Context에 원하는 Strategy 구현체를 주입한다.
- 클라이언트는 Context를 실행한다.
- Context는 고정된 로직을 시작한다.
- 고정 로직 중간에 strategy.call()을 호출해서 전략을 실행한다.
- Context는 나머지 로직을 실행하고 종료된다.
📝 전략 패턴의 두 가지 방식
1) 첫 번째 방식: 선조립 후 실행
전략을 context 생성 시점에 주입하는 방식이다.
public interface Strategy {
void 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 실행");
}
}
@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);
}
}
이 방식의 단점은 한 번 조립하면 전략을 변경하기 어렵다는 점이다.
특히 Context가 싱글톤이라면 동시성 문제가 발생할 수 있다.
예를 들어 여러 스레드가 동시에 같은 Context 인스턴스를 사용하고, 내부 전략이 변경되면 의도하지 않은 전략이 실행될 수 있다. 따라서 상태를 가지지 않는 StateLess 방식으로 Context를 설계해야 한다.
📗 참고
싱글톤 Context에서 전략 패턴 사용 시 동시성 문제
전략 패턴을 사용할 때 Context를 싱글톤으로 설계하면 동시성 문제가 발생할 수 있다. 특히 Context 내부에 Strategy를 필드로 가지고 있고, setStrategy()로 전략을 변경하는 구조일 경우 문제의 소지가
dev-jk93.tistory.com
🔥 전략 패턴 표현의 변천사
1. 인터페이스 구현체를 context에 전달
void strategyV2() {
Strategy strategyLogic1 = new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
};
ContextV1 context1 = new ContextV1(strategyLogic1);
context1.execute();
Strategy strategyLogic2 = new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
};
ContextV1 context2 = new ContextV1(strategyLogic2);
context2.execute();
}
2. 익명 내부 클래스 바로 전달
void strategyV3() {
ContextV1 context1 = new ContextV1(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
});
context1.execute();
ContextV1 context2 = new ContextV1(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
});
context2.execute();
}
3. 람다식 사용
void strategyV4() {
ContextV1 context1 = new ContextV1(() -> log.info("비즈니스 로직1 실행"));
context1.execute();
ContextV1 context2 = new ContextV1(() -> log.info("비즈니스 로직2 실행"));
context2.execute();
}
※ 람다식은 메서드가 단 하나만 있는 함수형 인터페이스에서만 사용할 수 있다.
2) 두 번째 방식: 실행 시 전략 전달 (템플릿 콜백 패턴)
실행할 때마다 전략을 파라미터로 넘겨주는 방식이다. 전략을 유연하게 변경할 수 있다는 장점이 있지만, 실행할 때마다 매번 넘겨줘야 한다는 점이 단점이다.
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);
}
}
void strategyV1() {
ContextV2 context = new ContextV2();
context.execute(new StrategyLogic1());
context.execute(new StrategyLogic2());
}
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;
}
}
전략 패턴은 템플릿 메서드 패턴보다 더 유연하고, 의존성 주입 방식과 잘 어울리는 구조이다. 특히, 람다와 템플릿 콜백 방식까지 이해하면 다양한 상황에서 전략 패턴을 손쉽게 활용할 수 있다.
'Design Pattern' 카테고리의 다른 글
프록시 패턴 정리 (0) | 2025.04.01 |
---|---|
[디자인 패턴] 템플릿 메서드 패턴 (0) | 2025.03.24 |
[디자인 패턴] Builder 패턴 (0) | 2025.03.23 |
댓글