dev.Log
전략 패턴 본문
템플릿 메서드 패턴은 부모클래스에 변하지 않는 템플릿을 두고, 변하는 부분을 자식 클래스에 두어서 상속을 사용해서 문제를 해결했다.
전략 패턴은 변하지 않는 부분을 'Context'라는 곳에 두고, 변하는 부분을 'Strategy'라는 인터페이스를 만들고 해당 인터페이스를 구현하도록 해서 문제를 해결한다. 상속이 아니라 위임으로 문제를 해결하는 것이다.
전략 패턴에서 'Context'는 변하지 않는 템플릿 역할을 하고, 'Strategy'는 변하는 알고리즘 역할을 한다.
GOF 디자인 패턴에서 정의한 전략패턴의 의도는 다음과 같다.
"알고리즘 제품군을 정의하고 각각의 캡슐화하여 상호 교환 가능하게 만들자. 전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다."
방법 1. Context의 필드에 Strategy를 주입해서 사용하기
public interface Strategy {
void call();
}
@Slf4j
public class StrategyLogic1 implements Strategy {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
}
/**
* 필드에 전략을 보관하는 방식
*/
@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은 변하지 않는 로직을 가지고 있는 템플릿 역할을 하는 코드이다. 전략패턴에서는 이것을 컨텍스트 (문맥)이라 한다.
쉽게 이야기해서 문맥은 크게 변하지 않지만, 그 문맥속에서 strategy를 통해 일부 전략이 변경된다 생각하면 된다.
Context는 내부에 Strategy strategy 필드를 가지고 있다. 이 필드에 변하는 부분인 'Strategy'의 구현체를 주입하면 된다.
전략 패턴의 핵심은 'Context'는 'Strategy' 인터페이스에만 의존한다는 점이다. 덕분에 'Strategy'의 구현체를 변경하거나 새로 만들어도 'Context' 코드에는 영향을 주지 않는다.
스프링에서 의존관계 주입에서 사용하는 방식이 바로 전략패턴이다.
/**
* 전략 패턴 사용
*/
@Test
void strategyV1() {
StrategyLogic1 strategyLogic1 = new StrategyLogic1();
ContextV1Test contextV1 = new ContextV1Test(strategyLogic1); //전략주입
contextV1.execute();
}
익명 내부 클래스 사용
@Test
void strategyV2() {
Strategy strategyLogic1 = new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1실행");
}
}
ContextV1 contextV1 = new ContextV1(strategyLogic1); //의존성주입
contextV1.execute(); //실행
}
@Test
void strategyV3() {
ContextV1 contextV1 = new ContextV1(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
});
contextV1.execute();
}
람다사용
@Test
void strategyV4() {
ContextV1 context1 = new ContextV1(() -> log.info("비즈니스 로직1 실행"));
contextV1.execute();
ContextV1 context2 = new ContextV1(() -> log.info("비즈니스 로직2 실행"));
context2.execute();
}
익명 내부 클래스를 자바7부터 제공하는 람다로 변경할 수 있다. 람다로 변경하려면 인터페이스에 메서드가 1개만 있으면 된다.
"선조립 후실행"
Context와 Strategy를 조립해두고 Context를 실행!
우리가 스프링으로 애플리케이션을 개발할 때 애플리케이션 로딩 시점에 의존관계 주입을 통해 필요한 의존관계를 모두 맺어두고 난 다음에 실제 요청을 처리하는 것과 같은 원리이다. 이 방식의 단점은 'Context'와 'Strategy'를 조립한 이후에는 전략을 변경하기가 번거롭다는 점이다. 물론 'Context'에 'setter'를 제공해서 'Strategy'를 넘겨받아 변경하면 되지만, 'Context'를 싱글톤으로 사용할 때는 동시성 이슈등 고려할 점이 많다. 그래서 전략을 실시간으로 변경해야 하면 차라리 이전에 개발한 테스트 코드처럼 'Context'를 하나 더 생성하고 그곳에 다른 'Strategy'를 주입하는 것이 더 나은 선택일 수 있다.
이렇게 먼저 조립하고 사용하는 방식보다 더 유연하게 전략 패턴을 사용하는 방법은 없을까?
방법 2. 파라미터로 Strategy를 전달받아 Context 실행하기
@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);
}
}
CotextV2는 전략을 필드로 가지지 않고, execute가 호출될 때마다 항상 파라미터로 전략을 전달받는다.
@Slf4j
public class ContextV2Test {
/**
* 전략패턴적용
*/
@Test
void strategyV1() {
ContextV2 context = new ContextV2();
context.execute(new StrategyLogic1());
context.execute(new StrategyLogic2());
}
}
클라이언트는 Context를 실행하는 시점에 원하는 Strategy를 전달할 수 있따. 따라서 이전 방식과 비교해서 원하는 전략을 더욱 유연하게 변경할 수 있다.
/*
* 전략 패턴 익명 내부 클래스
*/
@Test
void strategyV2() {
ContextV2 context = new ContextV2();
context.execute(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
});
}
'BACKEND.* > JAVA' 카테고리의 다른 글
Spring 1.X 대 보안취약점과 3.X와 비교 (0) | 2024.05.31 |
---|---|
템플릿 콜백 패턴 (0) | 2024.05.31 |
템플릿 메서드 패턴 (0) | 2024.05.31 |
동시성제어 - ThreadLocal (0) | 2024.05.29 |
Virtual Thread vs Thread (1) | 2024.04.10 |