인프런 스프링 - 고급편 강의 내용을 참고하여 정리한 내용입니다.
지난 포스팅에서 알아본 탬플릿 메서드 패턴은 부모 클래스에 변하지 않는 템플릿을 두고, 변하는 부분을 자식 클래스에 두어서 상속을 사용해서 문제를 해결했다.
전략 패턴은 변하지 않는 부분을 Context 라는 곳에 두고, 변하는 부분을 Strategy 라는 인터페이스를 만들고 해당 인터페이스를 구현하도록 해서 문제를 해결한다.
context를 상속하는 것이 아니라 context가 strategy 인터페이스에 의존하고, 구현체에 전략을 위임함으로써 문제를 해결하는 것이다.
전략 패턴의 구조는 아래와 같다.
상속 구조가 아니기 때문에 의존 방향도 다르다. context와 구현체 모두 strategy 인터페이스라는 추상 모델에 의존하고 있다. 이는 DIP(의존 역전 원칙)을 지키는 방법이기도 하다.
class Context(
private val strategy: Strategy,
) {
fun execute() {
val startTime = System.currentTimeMillis()
strategy.call() // 위임
val endTime = System.currentTimeMillis ()
val resultTime = endTime - startTime;
log.info("resultTime={}", resultTime)
}
companion object : Log
}
Context는 변하지 않는 로직을 가지고 있는 템플릿 역할을 하는 코드이다. 전략 패턴에서는 이것을 컨텍스트(문맥)이라 한다.
쉽게 이야기해서 컨텍스트(문맥)는 크게 변하지 않지만, 그 문맥 속에서 외부로부터 주입받는 strategy 를 통해 일부 전략이 변경된다고 할 수 있다.
interface Strategy {
fun call(): String
}
class StrategyLogic1 : Strategy {
override fun call(): String {
log.info("전략1")
return "전략1"
}
companion object : Log
}
class StrategyLogic1 : Strategy {
override fun call(): String {
log.info("전략1")
return "전략1"
}
companion object : Log
}
@Test
fun strategy() {
val strategy1 = StrategyLogic1()
val context = Context(strategy1)
context.execute() // 전략 1 실행
val strategy2 = StrategyLogic2()
val context = Context(strategy2)
context.execute() // 전략 2 실행
}
전략 패턴도 내부 익명 클래스를 사용할 수 있는데 이를 통해 구현체의 인스턴스를 생성함과 동시에 클래스를 정의할 수 있다.
@Test
fun strategy_anonymous() {
val strategy = object : Strategy {
override fun call(): String {
log.info("전략3 실행")
return "전략3"
}
}
val context = Context(strategy)
context.execute() // 전략 3 실행
}
전략패턴은 변하지 않는 부분을 context쪽에 두고 변하는 부분을 Strategy로 추출하여 이를 조립하는 방식으로 구현한다. 이러한 방식은 의존의 방향이 템플릿 메서드 패턴과는 반대로 변하지 않는 부분에서 변하는 부분으로 향하지만, 구현체에 직접 의존하는 것이아닌 추상화된 인터페이스에 의존하게 함으로써 변경의 유연함을 확보할 수 있다.
이로써 전략 패턴을 통해 SOLID 원칙의 OCP와, DIP원칙이 지켜지고 있는 모습을 볼 수 있다.
추가적으로 변하는 부분과, 변하지 않는 부분의 책임을 분리함으로써 SRP까지 지켜진다고 볼 수 있다.