전략 패턴은 변하는 부분과 변하지 않는 부분을 분리하는 템플릿 메소드 패턴과 유사한 기능을 하면서,
구현 시 상속으로 인해 발생되는 템플릿 메소드 패턴의 단점을 보완할 수 있는 디자인 패턴이다.
전략 패턴은 변하지 않는 부분(템플릿)을 Context 클래스에 구현하고,
Strategy 인터페이스의 구현체에서 변하는 부분을 구현하는 디자인 패턴이다.
Context 클래스와 Strategy 인터페이스의 구현체는 Strategy 인터페이스만 의존할 뿐이며, 서로는 독립적이다.
이러한 구조를 통해 변하는 부분을 구현할 때 불필요함에도 변하지 않는 부분을 의존해야했던 템플릿 패턴 메소드의 단점을 극복할 수 있다.
전략 패턴
전략 패턴은 전략을 필드에 보관하는 방식, 파라미터로 전달받는 방식 두 가지로 구현할 수 있다.
전략 패턴 - 필드 보관 방식
우선 전략을 필드에 보관하는 방식으로 구현하는 코드들을 살펴보자.
ContextV1
ContextV1은 변하지 않는 부분, 즉 템플릿이 작성되는 클래스이다.
현재 예시에서는 시간 측정 로직이 작성되어 있다.
ContextV1은 전략의 역할을 하는 Strategy 변수를 필드로 갖고 있다.
그리고 생성자를 통해 Strategy 구현체를 주입한다.
그리고 execute()가 구현되어 있다.
execute()는 변하지 않는 템플릿의 역할을 하며, 내부에서 변하는 부분인 전략을 실행하는 Strategy.call()을 호출한다.
// 전략 패턴은 변하지 않는 부분인 Context와 변하는 부분인 Strategy의 구현체가 Strategy에만 의존하고 있다.
// 변하는 부분을 구현하기 위해, 변하지 않는 부분을 의존해야 했던 템플릿 메소드 패턴과의 차이이다.
// 전략 패턴 - 필드에 전략을 보관하는 방식
@Slf4j
public class ContextV1 {
private Strategy 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, StrategyLogic
Strategy는 변하는 부분이 재정의될 메소드인 call()이 정의되어 있는 인터페이스이다.
StrategyLogic은 Strategy의 구현체로 call()을 목적에 맞게 재정의한다.
StrategyLogic1은 비즈니스 로직1이 실행되도록 재정의 되어 있으며,
StrategyLogic2는 비즈니스 로직2가 실행되도록 재정의 되어 있다.
public interface Strategy {
// 변하는 부분이 재정의될 메소드
void call();
}
// 변하는 부분이 구현되는 StrategyLogic은 Strategy만 의존하고 있다.
// 변하지 않는 부분인 Context가 변경되더라도, 전혀 영향을 받지 않는다.
@Slf4j
public class StrategyLogic1 implements Strategy {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
}
// 변하는 부분이 구현되는 StrategyLogic은 Strategy만 의존하고 있다.
// 변하지 않는 부분인 Context가 변경되더라도, 전혀 영향을 받지 않는다.
@Slf4j
public class StrategyLogic2 implements Strategy {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
}
ContextV1Test
ContextV1Test에는 ContextV1과 Strategy의 구현체를 통해 전략 패턴을 실행하는 테스트 코드들이 작성되어 있다.
strategyV1() 내부를 살펴보자.
Strategy의 구현체인 StrategyLogic1, StrategyLogic2 변수들을 선언한다.
그리고 StrategyLogic1, StrategyLogic2 객체를 각각 필드에 보관하는 ContextV1 변수들을 선언했다.
각 ContextV1 변수들의 execute()를 호출하면 execute() 내부에서 strategy.call()이 호출되기 때문에 분리되었던 변하지 않는 부분과 변하는 부분이 통합되어 실행된다.
strategyV3() 내부를 살펴보자.
strategyV3()는 익명 내부 클래스를 통해 Strategy의 구현체를 필요한 전략에 따라 매번 생성해둬야 했던 단점을 보완한 코드이다.
마지막으로 strategyV4() 내부를 살펴보자.
strategyV4()는 람다식을 통해 익명 내부 클래스를 생성하는 코드를 더욱 간소화한 코드이다.
@Slf4j
public class ContextV1Test {
@Test
void strategyV0() {
logic1();
// 비즈니스 로직1 실행
// resultTime=4
logic2();
// 비즈니스 로직2 실행
// resultTime=0
}
// 핵심기능인 비즈니스 로직과 부가기능인 시간측정 로직이 혼재되어있다.
// 시간측정 로직은 변하지 않으며, logic2에도 중복 작성되어있다.
// 또한 부가기능의 변경이 있을 경우, 부가기능이 적용된 메소드를 모두 수정해야한다.
private void logic1() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직1 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
// 핵심기능인 비즈니스 로직과 부가기능인 시간측정 로직이 혼재되어있다.
// 시간측정 로직은 변하지 않으며, logic1에도 중복 작성되어있다.
// 또한 부가기능의 변경이 있을 경우, 부가기능이 적용된 메소드를 모두 수정해야한다.
private void logic2() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직2 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
// 전략 패턴 사용
// Context의 필드에 전략을 보관
@Test
void strategyV1() {
StrategyLogic1 strategyLogic1 = new StrategyLogic1();
ContextV1 context1 = new ContextV1(strategyLogic1);
context1.execute();
// 비즈니스 로직1 실행
// resultTime=4
StrategyLogic2 strategyLogic2 = new StrategyLogic2();
ContextV1 context2 = new ContextV1(strategyLogic2);
context2.execute();
// 비즈니스 로직2 실행
// resultTime=0
}
// 전략 패턴 사용 - 익명 내부 클래스 - 인라인
// Context의 필드에 전략을 보관
@Test
void strategyV3() {
ContextV1 context1 = new ContextV1( new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
});
context1.execute();
// 비즈니스 로직1 실행
// resultTime=4
ContextV1 context2 = new ContextV1( new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
});
context2.execute();
// 비즈니스 로직2 실행
// resultTime=0
}
// 전략 패턴 사용 - 익명 내부 클래스 - 람다
// Context의 필드에 전략을 보관
@Test
void strategyV4() {
ContextV1 context1 = new ContextV1(() -> log.info("비즈니스 로직1 실행"));
context1.execute();
// 비즈니스 로직1 실행
// resultTime=4
ContextV1 context2 = new ContextV1(() -> log.info("비즈니스 로직2 실행"));
context2.execute();
// 비즈니스 로직2 실행
// resultTime=0
}
}
전략 패턴 - 파라미터 전달 방식
위에서 보았던 필드 보관 방식은 Context.execute()를 실행하기 전에 Strategy 구현체를 주입해두는 방식이다.
즉 선 조립, 후 실행 방법이라고 할 수 있다.
이러한 방식의 단점은 조립이 완료된 이후에 전략을 변경하기 어렵다는 것이다.
만약 Context가 스프링의 싱글톤 빈으로 관리된다고 가정해보자. 하나의 Context는 하나의 Strategy 구현체만 사용할 수 있게 된다. 다른 Strategy 구현체를 사용하고 싶다면 그에 따라 Context도 추가로 생성해야한다.
이러한 단점을 보완하여 유연하게 전략을 변경하면서 사용할 수 있는 방법이, 파라미터 전달 방식으로 전략 패턴을 구현하는 것이다.
ContextV2
ContextV2는 ContextV1과 마찬가지로 변하지 않는 부분, 즉 템플릿이 작성되는 클래스이다.
ContextV1과의 차이점은 Strategy 구현체를 생성자로 주입 받아 필드에 보관하지 않고,
템플릿이 작성된 execute()의 파라미터로 전달 받는 것이다.
// 전략 패턴은 변하지 않는 부분인 Context와 변하는 부분인 Strategy의 구현체가 Strategy에만 의존하고 있다.
// 변하는 부분을 구현하기 위해, 변하지 않는 부분을 의존해야 했던 템플릿 메소드 패턴과의 차이이다.
// 전략 패턴 - 전략을 파라미터로 전달 받는 방식
@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);
}
}
ContextV2Test
ContextV2Test는 파라미터 전달 방식으로 구현된 전략 패턴이 어떻게 사용되는지를 보여주는 테스트 코드가 작성되어있다.
다수의 테스트들이 작성되어있는데, ContextV1Test와 마찬가지로 Strategy의 일반적인 구현체를 사용하거나, 익명 내부 클래스를 사용하거나, 람다식을 사용하는 등의 방법들을 보여준다.
이것은 사실 중요한 것은 아니다.
중요한 것은 하나의 Context 객체를 통해 하나의 Strategy 구현체만을 사용할 수 있었던 필드 보관 방식과 달리,
파라미터 전달 방식에서는 하나의 Context 객체로 여러 개의 Strategy 구현체를 사용할 수 있다는 것이다.
@Slf4j
public class ContextV2Test {
// 전략 패턴 사용
// 전략을 Context.execute()의 파라미터로 전달 받음
@Test
void strategyV1() {
ContextV2 context = new ContextV2();
context.execute(new StrategyLogic1());
// 비즈니스 로직1 실행
// resultTime=3
context.execute(new StrategyLogic2());
// 비즈니스 로직2 실행
// resultTime=0
}
// 전략 패턴 사용 - 익명 내부 클래스
// 전략을 Context.execute()의 파라미터로 전달 받음
@Test
void strategyV2() {
ContextV2 context = new ContextV2();
context.execute(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
});
// 비즈니스 로직1 실행
// resultTime=3
context.execute(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
});
// 비즈니스 로직2 실행
// resultTime=0
}
// 전략 패턴 사용 - 익명 내부 클래스 - 람다
// 전략을 Context.execute()의 파라미터로 전달 받음
@Test
void strategyV3() {
ContextV2 context = new ContextV2();
context.execute(() -> log.info("비즈니스 로직1 실행"));
// 비즈니스 로직1 실행
// resultTime=3
context.execute(() -> log.info("비즈니스 로직2 실행"));
// 비즈니스 로직2 실행
// resultTime=0
}
}
어떤 것이 더 좋은 방법인가?
필드 보관 방식은 Context 객체가 생성될 때 이미 특정 Strategy 구현체와 조립이 완료된다.
따라서 Context.execuete()를 호출할 때 별다른 작업 없이 단순히 호출만 해주면된다.
하지만 이후에 Strategy 구현체를 변경하기 어렵다는 단점이 있다.
파라미터 전달 방식은 Context 객체를 생성하고 Context.execute()를 호출 할 때 마다 사용할 Strategy 구현체를 선택할 수 있다.
따라서 하나의 Context 객체를 통해 다양한 Strategy 구현체를 사용할 수 있다.
하지만 Context.execute()를 호출 할 때 마다 Strategy 구현체를 선택해줘야한다는 단점이 있다.
사실 전략 패턴을 사용하는 이유는 변하지 않는 부분인 템플릿을 미리 작성해두고,
템플릿 내부에서 변하는 부분을 유연하게 변경해가면서 사용하기 위함이다.
따라서 전략 패턴의 목적에 맞는 방법은 파라미터 전달 방식이라고 할 수 있다.