전략 패턴
이전 장에서 살펴본 템플릿 메서드 패턴은 상속을 받는다는 단점이 있다.
자식 클래스 입장에서는 부모 클래스의 기능을 전혀 사용하지 않는데, 부모 클래스를 알아야하는 단점이 있다.
템플릿 메서드 패턴과 비슷한 역할을 하면서, 상속의 단점을 제거한 디자인 패턴이 바로 전략패턴이다.
전략 패턴 - 시작
package hello.advanced.trace.strategy;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
public class ContextV1Test {
@Test
void strategyV0() {
logic1();
logic2();
}
private void logic1() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직 1 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
private void logic2() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직 2 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
- 이전에 사용했던 기존 예제를 바탕으로 전략 패턴을 적용해보자.
전략 패턴 - 예제 1
템플릿 메서드 패턴은 부모에 변하지 않는 템플릿을 두고, 변하는 부분을 상속을 통해 자식이 구현하도록 했다.
전략 패턴은 변하지 않는 부분을 Context
에 두고, 변하는 부분을 Strategy
라는 인터페이스를 통해 구현하도록 문제를 해결한다. 상속이 아니라 위임으로 문제를 해결한다.
Strategy 인터페이스
public interface Strategy {
void call();
}
- 변하는 알고리즘 역할을 하는 인터페이스를 생성하자.
StrategyLogic 구현체
@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 실행");
}
}
ContextV1
@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);
}
}
ContextV1
은 변하지 않는 로직을 갖고 있는 템플릿 역할을 한다.- 전략 패턴에선 이것을 컨텍스트(문맥)이라고 부른다.
Context
내부에는 전략 필드를 가지고 있다. 이 필드에 변하는 부분의 구현체가 주입되는 것이다.
전략 패턴의 핵심은 전략 인터페이스에 의존한다는 점이다. 덕분에 구현체의 변경에 대해 영향을 받지 않는다. 스프링 DI 방식이 바로 전략 패턴과 동일하다.
ContextV1Test - 추가
@Test
void strategyV1() {
Strategy strategyLogic1 = new StrategyLogic1();
ContextV1 contextV1 = new ContextV1(strategyLogic1);
contextV1.execute();
Strategy strategyLogic2 = new StrategyLogic2();
ContextV1 contextV2 = new ContextV1(strategyLogic2);
contextV2.execute();
}
- 전략 패턴을 통해 구현체를 주입해주자.
Context
에 원하는Strategy
구현체를 주입한다.- 클라이언트는 컨텍스트를 실행한다.
context
는 로직을 시작하고,strategy.call()
호출을 통해 주입 받은 로직을 실행한다.context
는 나머지 로직을 실행한다.
전략 패턴 - 예제2
전략 패턴에도 템플릿 메서드 패턴과 같이 익명 내부 클래스를 적용해보자.
ContextV1Test - 추가
@Test
void strategyV2() {
Strategy strategyLogic1 = new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
};
log.info("strategyLogic1={}", strategyLogic1.getClass());
ContextV1 contextV1 = new ContextV1(strategyLogic1);
contextV1.execute();
Strategy strategyLogic2 = new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
};
log.info("strategyLogic2={}", strategyLogic2.getClass());
ContextV1 contextV2 = new ContextV1(strategyLogic2);
contextV2.execute();
}
- 익명 내부 클래스를 통해 구현체 클래스 생성 없이 내부적으로 사용하여 적용할 수 있다.
ContextV1Test - 간략하게 개선
@Test
void strategyV3() {
ContextV1 contextV1 = new ContextV1(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
});
contextV1.execute();
ContextV1 contextV2 = new ContextV1(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
});
contextV2.execute();
}
- 익명 내부 클래스를 다음과 같이 간략하게 표현할 수 있다.
ContextV1Test - 람다 적용
@Test
void strategyV4() {
ContextV1 contextV1 = new ContextV1( () -> log.info("비즈니스 로직1 실행"));
contextV1.execute();
ContextV1 contextV2 = new ContextV1(() -> log.info("비즈니스 로직2 실행"));
contextV2.execute();
}
- 자바 8버전 이상이라면 익명 내부 클래스를 지원하는 람다로 변경할 수 있다.
- 람다로 변경하려면 인터페이스에 메서드가 1개만 있어야 된다는 점을 주의하자.
전략 패턴 - 예제 3
ContextV1 contextV1 = new ContextV1( () -> log.info("비즈니스 로직1 실행")); // 조립 완성
contextV1.execute(); // 실행
예제 2에서는 Context
필드에 Strategy
를 주입해서 사용했다.
이 방식은 선조립 후실행 방식으로, 컨텍스트와 전략 간에 의 존 관계를 맺어두고 실행하는 원리이다.
조립한 이후에는 전략 변경이 번거롭다는 단점을 개선하기 위해 전략을 파라미터로 전달해서 사용해보자.
ContextV2
@Slf4j
public class ContextV2 {
/* 필드 전략 대신 파라미터로 전달
private Strategy strategy;
public ContextV2(Strategy strategy) {
this.strategy = strategy;
}
*/
public void execute(Strategy strategy) {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
strategy.call(); // 위임
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
- ContextV2에서는 전략을 파라미터로 전달하여 생성하자.
ContextV2Test
@Test
void strategyV2() {
ContextV2 context = new ContextV2();
context.execute(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
});
context.execute(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
});
}
@Test
void strategyV3() {
ContextV2 context = new ContextV2();
context.execute(() -> log.info("비즈니스 로직 1 실행"));
context.execute(() -> log.info("비즈니스 로직 2 실행"));
}
- 파라미터로 넘어온 전략을 다음과 같이 실행하여 테스트해볼 수 있다.
Context
와 Strategy
를 선조립 후 실행하는 방식이 아니라, Context
를 실행할 때마다 전략을 파라미터로 전달했다.
클라이언트는 Context
를 실행하는 시점에 원하는 Strategy
를 전달할 수 있다.
- 클라이언트는
Context
를 실행하면서 파라미터로Strategy
를 전달한다. Context
는 execute() 로직을 실행하면서 전략의 call() 메서드를 실행하고, 종료된다.
정리
- 템플릿 메서드 패턴의 상속 문제를 전략 패턴을 통해 인터페이스화를 통한 위임으로 해결할 수 있다.
- 전략 패턴에서
Context
는 변하지 않는 템플릿 역할을 하고,Strategy
는 변하는 알고리즘 역할을 한다. Context
와Strategy
간에 의존 관계를 유연하게 설계할 수 있도록 고민하자.
본 포스팅은 인프런 - 스프링 핵심 원리 - 고급편을 참고하여 정리하였습니다.