Strategy Pattern (전략 패턴) feat.Spring
다양한 디자인 패턴 중 Strategy Pattern에 대해 알아보고 Spring에서는 어떻게 사용할 수 있는지 알아보자.
Strategy Pattern(전략 패턴)이란?
전략 패턴은 실행 중에 알고리즘을 선택할 수 있게 하는 소프트웨어 디자인 패턴이다.
전략 패턴은
- 특정한 계열의 알고리즘들을 정의하고
- 각 알고리즘을 캡슐화하며
- 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다.
by Wiki
말이 어렵긴 한데 한마디로 정의하면 컨텍스트에서 알고리즘을 분리하는 설계이다.
즉, 특정 기능을 수행할 때 다양한 알고리즘이나 비즈니스 로직이 쓰일 수 있다면 알고리즘이나 로직만 분리하는 설계이다.
- Context : 전략 패턴을 실제로 사용하는 부분.
- Strategy : 인터페이스나 추상 클래스로 알고리즘을 호출하는 방식을 명시
- Concreate Strategy : 전략 패턴에서 명시한 알고리즘을 실제로 구현하는 클래스
Strategy Pattern(전략 패턴) 예시
전략 패턴의 예시를 보기 전에 전략 패턴이 사용되지 않은 코드를 보자.
상황
음식을 먹는 'Eater' 클래스가 있다.
이 클래스는 먹을 수 없는 음식은 먹지 않으며, 고체일 경우 먹고, 고체가 아닐 경우 마신다.
fun main(args: Array<String>) {
val eater = Eater()
eater.eat(Banana()) // "Banana을 먹는다." 출력
}
interface Food {
val foodName: String
val isSolid: Boolean
val isEatable: Boolean
}
class Banana: Food {
override val foodName = "Banana"
override val isSolid = true
override val isEatable = false
}
class Eater {
fun eat(food: Food) {
if (food.isEatable){
println("${food.foodName}을 먹지 않는다.")
} else if(food.isSolid) {
println("${food.foodName}을 먹는다.")
} else {
println("${food.foodName}을 마신다.")
}
}
}
코드에서 알 수 있듯, 음식을 먹기 위해서는 내부에서 무수한 if-else 분기를 타야 한다.
이런 경우 새로운 음식이 추가되어 음식을 먹는 새로운 방법이 추가될 경우 계속해서 if-else를 추가해야 한다.
이렇게 방법이 계속해서 추가될 경우 코드는 지저분해지고 시간이 지날수록 코드 분석은 어려워진다.
위의 코드를 전략 패턴을 적용해서 바꾸면 아래와 같다.
fun main(args: Array<String>) {
val eater = Eater()
eater.eat(Banana()) // Banana을 먹는다.
eater.eat(Juice()) // Juice을 마신다.
eater.eat(AirPod()) // AirPod Pro은 먹을 수 없다.
}
class Banana: Eatable {
override val foodName = "Banana"
override fun eat() {
println("${foodName}을 먹는다.")
}
}
class Juice: Eatable {
override val foodName = "Juice"
override fun eat() {
println("${foodName}을 마신다.")
}
}
class AirPod: Eatable {
override val foodName = "AirPod Pro"
override fun eat() {
println("${foodName}은 먹을 수 없다.")
}
}
class Eater {
fun eat(food: Eatable) {
food.eat()
}
}
interface Eatable {
val foodName: String
fun eat()
}
'Food' 대신 'Eatable'이라는 인터페이스를 정의하고 음식을 먹는 방법인 'eat()'을 정의한다.
음식 별로 각자 먹는 방법(전략)인 'eat()'매서드를 스스로 구현하도록 한다.
이렇게 구현하면 컨텍스트쪽 코드의 변경 없이 새로운 음식들이 추가될 수 있는 장점이 있다.
즉, 새로운 음식이 추가될 때마다 클래스를 하나씩 추가하면 된다.
Spring에서 Strategy Pattern(전략 패턴) 사용하기
Spring을 통해 백엔드 서버를 개발할 때 전략 패턴의 장점을 그대로 사용할 수 있다.
위의 예제를 그대로 Spring에 적용하여 'EatFoodService'라는 서비스에서 사용해보자.
interface Eatable {
val foodName: String
fun eat()
}
@Component
class Banana: Eatable {
override val foodName = "Banana"
override fun eat() {
println("${foodName}을 먹는다.")
}
}
@Component
class Juice: Eatable {
override val foodName = "Juice"
override fun eat() {
println("${foodName}을 마신다.")
}
}
@Component
class AirPod: Eatable {
override val foodName = "AirPod Pro"
override fun eat() {
println("${foodName}은 먹을 수 없다.")
}
}
대부분의 코드는 위의 예제와 비슷하지만 Banana, Juice, AirPod클래스에 @Component 어노테이션을 사용하여 Bean으로 등록한다.
@Service
class EatFoodService(
private val eatStrategyFactory: EatStrategyFactory
) {
fun eat(foodName: String) {
val strategy = eatStrategyFactory.findStrategy(foodName)
strategy.eat();
}
}
@Component
class EatStrategyFactory(
strategySet: Set<Eatable> //Eatable Interface를 상속한 Bean들을 DI해준다.
) {
private val strategies = mutableMapOf<String, Eatable>()
init {
setStrategy(strategySet)
}
private fun setStrategy(strategySet: Set<Eatable>) {
strategySet.forEach { strategies[it.foodName] = it }
}
fun findStrategy(foodName: String): Eatable {
return strategies[foodName] ?: throw Error()
}
}
'EatFoodService'에서 음식별로 알맞은 전략을 찾기 위한 'EatStrategyFactory'클래스를 만들어준다.
이때 생성자에서 'Set<Eatable>'을 파라미터로 받으면 'Eatable' 인터페이스를 상속받아 구현된 Bean들을 모두 Dependancy Injection 해준다.
'EatFoodService'에서는 'EatStrategyFactory'를 통해 적절한 전략을 찾아 'eat'매서드를 실행한다.
Test
@SpringBootTest
class EatFoodServiceTest {
@Autowired
lateinit var eatFoodService: EatFoodService
@Test
fun eatFoodTest() {
eatFoodService.eat("Banana") // Banana을 먹는다.
eatFoodService.eat("Juice") // Juice을 마신다.
eatFoodService.eat("AirPod Pro") // AirPod Pro은 먹을 수 없다.
}
}
Spring에서 전략 패턴을 더 전략 패턴스럽게 사용할 수 있다.