이전 포스팅에서 템플릿 메서드 패턴에 대해서 학습하였습니다. 템플릿 메서드 패턴의 가장 큰 문제는 자식 클래스가 부모 클래스를 상속 받음으로써 부모 클래스에게 강하게 의존한다는 것이 문제였는데요. 좋은 객체지향 설계 원칙에서 두 가지를 지키지 못하는 상황이 발생해 버린 것이죠. 그 두 가지는 다음과 같습니다.
- 상속보다는 구성을 활용한다.
- 인터페이스에 맞춰서 프로그래밍을 한다.
전략 패턴은 이러한 템플릿 메서드 패턴의 단점을 해결 한 방식입니다. 우선 정의부터 살펴보도록 할까요?
전략 패턴의 정의
알고리즘 제품군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만든다.
이 전략 패턴을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.
알고리즘 군
여기서 알고리즘 제품군 이라는 용어가 좀 생소하게 들릴 수 있는데요. 알고리즘 군이란 어떠한 행동에 대한 추상화를 의미합니다.
예를 들어서, 사람은 필기를 할 때 여러가지 방식으로 필기를 할 수 있는데요.
노트북 타이핑을 통해서 필기를 하는 사람이 있고, 직접 노트에 적어서 필기를 하는 사람이 있지요.
이러한 방식이 구체화 된 필기 방식이고, 이러한 행동들을 추상화하면 필기하기 가 되는 것입니다. 둘 다 필기를 하는 것은 동일하니까요.
이렇게 어떠한 행동에 대해서 추상화를 한 것 그것을 알고리즘 군이라고 해요.
우리는 알고리즘 군을 정해서 즉, 추상화를 해서 구현체에 따라 다르게 사용할 수 있지요. 바로 다형성을 통해서 말이죠!
그럼 전략패턴이 어떤 구조를 가지고 있고 어떻게 구현되는 지에 대해서 한 번 살펴봅시다.
전략 패턴의 구조
전략패턴은 다음과 같은 구조를 띄고 있어요. 이전에 템플릿 메서드 패턴에서는 call()을 구현하는 자식 클래스가 상속을 받는 구조였지만,
이제는 execute()를 실행하는 클래스가 call()을 실행하는 interface를 구성하여 문제를 해결하는 모습을 볼 수 있고, 추상화 된 인터페이스에 대해 여러 개의 구현체를 가지고 있는 모습을 확인 할 수 있습니다.
하지만 이를 그림으로 보기 보다 코드로 보면 더 이해가 쉽겠죠? 코드로 먼저 한 번 살펴봅시다.
public interface Strategy {
void call();
}
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 void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
}
Strategy 인터페이스를 선언하고, 템플릿을 명시해놓은 곳에 Strategy를 구성한 것을 볼 수 있어요. 그리고 이러한 구성 클래스에 대해서 생성자를 통해서 구현체를 주입 받도록 하는 모습을 볼 수 있지요.
따라서, 해당 코드에 Strategy를 구현한 구현체를 주입하면, 템플릿은 동일하지만 비즈니스 로직이 다르게 실행 될 수 있겠죠?
Setter를 통한 알고리즘 군 교체 문제
그리고 헤드 퍼스트 디자인 패턴 교재에서는 Setter를 통해서 필요에 따라 동적으로 알고리즘 군을 바꿀 수 있는 것을 큰 장점으로 두고 있어요.
예를 들어보자면, 어느 날 갑자기 타이핑으로 필기를 하던 사람이, 타이핑으로 필기를 하면 손으로 필기를 하는 것보다 상대적으로 기억이 오래가지 않는다는 연구 결과를 보고 다시 손으로 직접 쓰면서 필기를 할 수 도 있겠죠? (사실 제 얘기입니다.)
교체가 된다면 어떻게 교체가 되는지 한 번 살펴보도록 할까요?
@Test
void strategyV4() {
Strategy strategyLogic1 = () -> log.info("노트북 타이핑으로 필기하기");
Strategy strategyLogic2 = () -> log.info("직접 손으로써서 필기하기");
ContextV1 contextV1 = new ContextV1(strategyLogic1);
contextV1.execute();
contextV1.setStrategy(strategyLogic2);
contextV1.execute();
}
02:15:04.435 [Test worker] INFO hello.advanced.trace.strategy.ContextV1Test - 노트북 타이핑으로 필기하기
02:15:04.437 [Test worker] INFO hello.advanced.trace.strategy.code.strategy.ContextV1 - resultTime=3
02:15:04.438 [Test worker] INFO hello.advanced.trace.strategy.ContextV1Test - 직접 손으로써서 필기하기
02:15:04.438 [Test worker] INFO hello.advanced.trace.strategy.code.strategy.ContextV1 - resultTime=0
짜잔, 구현체가 교체된 모습을 볼 수가 있고, 저희가 기존에 사용하던 템플릿은 그대로 사용할 수 있게 되었어요.
이렇게만 보면 Setter를 사용해서, 어떤 필요한 상황에 교체를 하는게 좋은데 어떤 문제가 발생을 할까요?
사실 이 문제는 특정 조건에서만 발생 할 수 있어요. 이전 포스팅에서 스프링컨테이너에 싱글톤으로 보관하는 객체로 인한 동시성 문제에 대해서 말씀을 드렸었죠. 전략 패턴 또한 동일하게 구성에 대한 생성자 주입을 다른 구현체로 바꿔버린다면, 자연스럽게 해당 패턴을 사용하던 다른 구현체들도 변경되는 상황이 되어버리는 것이지요.
따라서 스프링에서 만약 전략 패턴을 사용해야 할 일이 있다면, Setter를 사용해서 교체하지 않는 것이 제일이에요.
하지만, 그렇다고 방법이 없는 건 아니에요!
Setter를 통한 알고리즘 군 변경 문제 해결
Setter를 쓰지 않고 알고리즘 군 변경 문제를 해결할 수 있는데요. 바로 파라미터를 이용해서 구성을 주입받는 거에요.
여기서, 혹시 기존에 전략 패턴을 알고 계시던 분들이라면 이런 질문을 할 수가 있을텐데요
구성을 사용해서, 알고리즘 군을 정의하고 이에 따라 변경된 알고리즘을 사용 할 수 있도록 하는 것이 전략 패턴 아닌가?
우리는 디자인 패턴을 공부하다보면, 디자인 패턴의 틀에 얽매여서 공부를 하게 되는 경향이 있지요. 물론 처음 배울때에는 누구나 그럴 것이라고 생각해요. 하지만, 디자인 패턴을 공부하면서 가장 눈 여겨보아야 할 부분은 의도입니다.
전략 패턴에서 가장 큰 의도는 알고리즘 군을 정의하고 이에 따라 변경된 알고리즘을 사용한다. 이 부분이겠지요?
따라서 다음과 같이 변경해서 사용을 할 수가 있어요.
public interface Strategy {
void call();
}
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);
}
}
이제 구현체를 파라미터에 전달해주면 Setter 방식을 사용하지 않고도, 변경된 알고리즘을 사용 할 수 있겠죠? 예시로 한 번 살펴봅시다.
@Test
void strategyV4() {
ContextV2 context = new ContextV2();
context.execute(() -> log.info("노트북 타이핑으로 필기하기"));
context.execute(() -> log.info("직접 손으로써서 필기하기"));
}
02:25:43.078 [Test worker] INFO hello.advanced.trace.strategy.ContextV2Test - 노트북 타이핑으로 필기하기
02:25:43.081 [Test worker] INFO hello.advanced.trace.strategy.code.strategy.ContextV2 - resultTime=4
02:25:43.082 [Test worker] INFO hello.advanced.trace.strategy.ContextV2Test - 직접 손으로써서 필기하기
02:25:43.082 [Test worker] INFO hello.advanced.trace.strategy.code.strategy.ContextV2 - resultTime=0
기존 코드에서는 직접 함수형 인터페이스를 구현하고 함수형 인터페이스의 참조 변수를 인자로 전달하는 방식을 택했는데, 이 번에는 람다를 사용해서 넣어봤습니다. 그리고 실행 결과를 살펴보면 위에서 실행한 결과와 큰 차이가 없다는 것을 볼 수 있지요.
만약 알고리즘 군의 변경이 필요하다면 되도록 전략 패턴의 파라미터 방식을 택해서 수정하는 것이 좋을 것 같습니다!
이번 포스팅에서는 전략 패턴에 대해서 학습을 하였는데요. 사실 이 파트에서 전략패턴도 중요하지만 더 중요한 것이 있어요.
원래 그런 것이 그런 것인가? 다른 방법은 없는 것인가? 에 대한 의심을 하고, 다른 해결 방안을 찾아보는 것 입니다.
특히 패턴 같은 것들을 사용하게 되면 기존의 틀에 얽매여서, 그에 맞게만 사용하려고 하는 경우가 많습니다. 하지만 그럴 때 마다 꼭 이렇게 해결해야 하는가에 대한 생각을 조금이라도 하면 또 더 좋은 방법을 찾을 수 있지 않을까요?
오늘의 포스팅은 여기까지 입니다.
'Language' 카테고리의 다른 글
[UML] 클래스 다이어그램 (2) | 2023.05.20 |
---|---|
gradle build vs Intellij IDEA build (0) | 2023.05.20 |
Lombok ToString 순환참조 방지하기 (1) | 2023.05.12 |
템플릿 메서드 패턴 (2) | 2023.04.15 |