Design Pattern

Strategy Pattern (전략 패턴) feat.Spring

예발이 2021. 11. 13. 22:41

다양한 디자인 패턴 중 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에서 전략 패턴을 더 전략 패턴스럽게 사용할 수 있다.