본문 바로가기

웹개발 - Back 관련/Architecture

[ GOF ] 전략 패턴 - Strategy Pattern with Koltin

Topic =  Strategy 패턴의 의도와 개념을 학습하고 Template Method 패턴과 Strategy 패턴 차이 정리하기

 

🔑 Purpose

전략 패턴 - 특정 알고리즘을 캡슐화하여서 Context 내에서 유연하게 알고리즘을 변경하여 사용할 수 있도록 하는 패턴

 


 

Strategy Pattern

 

[ 상황 1 ]

사용자들이 서비스를 이용하다가 악성 행위를 했을 때, 악성 유저 점수를 추가하고, 악성 점수 상향 처리에 걸린 시간을 로그로 남기고, 악성 유저 점수가 추가된 후에는 해당 회원의 악성 점수를 로그에 남긴다.

 

[ ❌ Strategy 패턴 미적용 예시 ]

private val logger =KotlinLogging.logger {  }
class User {
    private var count = 0
    
    fun execute() {
        val startTime = System.currentTimeMillis()
       
        count ++
        
        val resultTime = System.currentTimeMillis() - startTime
        logger.info { " 경고 카운트 추가 로직 실행 시간 - $resultTime " }
        logger.info { " 현재 경고 count = $count " }
    }
}

fun main {
    val userA = User()
    
    userA.execute()
}
  • 초기 알고리즘은 악성 점수 +1 해주는 알고리즘이 전부였지만, 특정 A 상황에는 경고카운트를 +1, B 상황에서는 경고 카운트를 +5, C 상황에서는 경고 카운트를 x 2 해주는 악성 점수 요구 사항이 추가되었다고 가정해보자.

 

private val logger =KotlinLogging.logger {  }
class User {
    private var count = 0
    
    fun executeA() {
        val startTime = System.currentTimeMillis()
        
        count ++
        
        val resultTime = System.currentTimeMillis() - startTime
        logger.info { " 경고 카운트 추가 로직 실행 시간 - $resultTime " }
        logger.info { " 현재 경고 count = $count " }
    }
    fun executeD() {
        val startTime = System.currentTimeMillis() // 중복
        
        count += 5
        
        val resultTime = System.currentTimeMillis() - startTime // 중복
        logger.info { " 경고 카운트 추가 로직 실행 시간 - $resultTime " } 중복
        logger.info { " 현재 경고 count = $count " } // 중복
    }
    fun executeC() {
        val startTime = System.currentTimeMillis() // 중복
        
        count *= 2
        
        val resultTime = System.currentTimeMillis() - startTime // 중복
        logger.info { " 경고 카운트 추가 로직 실행 시간 - $resultTime " } // 중복
        logger.info { " 현재 경고 count = $count " } // 중복
    }
}

fun main {
    val userA = User()
    
    userA.executeA()
    userA.executeB()
    userA.executeC()
    
}
  • 유저 악성점수 상향 로직 시간을 측정 하는 로직은 별도 메서드로 분리하기가 어려우므로 동일 코드가 중복되는 문제가 발생했다.

 

🚨 Template Method 패턴과 Strategy 패턴

  • 알고리즘의 뼈대를 정의하고, 알고리즘의 일부 단계를 하위 클래스에서 구현하도록 하는 Template method 패턴은 추상 클래스와 상속을 통해 하위 클래스에서 추상 메서드를 구현하도록 하는 것인데, 상속을 통해 구현하므로 새로운 알고리즘을 추가할 때 마다 새로운 클래스를 상속 받아 알고리즘을 정의하므로 유연하게 알고리즘을 갈아 끼울 수 없다.
  • ➡️그 대안으로 어떤 흐름 내에서 알고리즘 변동이 잦은 상황에는 탬플릿 매서드 패턴과 동일하게 로직 흐름 내에 변동되지 않는 로직은 Context 에 두고, 변경되는 로직(ConcreteStrategy)을 Strategy 로 캡슐화하여 분리해 Context가 시작될 때 추상화된 Strategy을 통해 변동되는 알고리즘을 적용하는 방법을 사용하게 되었다. 이 방법이 전략 패턴이다. 

 

 

[ 전략 패턴 적용 예제 ]

 

[ Strategy ]

interface StateStrategy {
    fun stateCall(count: Int): Int
}
  • 역할 : 특정 알고리즘을 정의하는 메서드를 선언한다. 인터페이스로 추상화된 다양한 알고리즘은 Context 내에서 동일한 방식으로 사용할 수 있도록 한다.

[ Concrete Strategy 구체적인 전략 ]

class ConcreteStrategyA : StateStrategy {
    override fun stateCall(count: Int): Int {
        return count + 1
    }
}

class ConcreteStrategyB : StateStrategy {
    override fun stateCall(count: Int) : Int{
        return count + 5
    }
}

class ConcreteStrategyB : StateStrategy {
    override fun stateCall(count: Int) : Int{
        return count * 2
    }
}
  • 역할 : Strategy 인터페이스를 구현하여 실제 알고리즘을 정의하는 것으로 다양한 알고리즘의 구체적인 구현을 캡슐화하여 필요에 따라 쉽게 교체할 수 있도록 한다.

 

[ Context - 파라미터 주입 방식 ]

private val logger = KotlinLogging.logger {  }
class StrategyContext {

    private var count = 0

    fun execute(strategy: StateStrategy) {
        val startTime = System.currentTimeMillis()

        count = strategy.stateCall(count)

        val resultTime = System.currentTimeMillis() - startTime
        logger.info { "경고 카운트 추가 로직 실행 시간 $resultTime" }
        logger.info { "현재 경고 count = $count" }
    }
}

 

  • 역할 : Strategy 인터페이스를 사용하여 알고리즘을 실행하는 클래스로, 어떤 구체화된 전략 객체를 사용할 지 결정하고, 그 객체의 메서드를 호출한다.

📗[ Strategy - 생성자 & Setter 주입 or 메서드 파라미터 주입 ]

 

전략 생성자 주입 - ( 필드 선언 )

  • Context 객체 생성 시점에 Strategy 를 주입 받고, 이후 변경 시에는 setter 메서드를 사용하는 방식으로 사용하는데, 이는 전략을 지정하고 나면, Context에 새로운 전략을 setter 하기 전에는 어떤 전략이 지정되었는지에 대한 명시성이 조금 떨어진다. 때문에 동적인 알고리즘 변경이 자주 일어나는 상황에는 적합하지 않다.

 

메서드 파라미터 주입

  • Contest의 메서드에서 ConcreteStrategy를 파라미터로 받아서 실행하는 방법으로, 필요에 따라 실행 시점에 여러 전략을 동적으로 선택할 수 있다. 

 

 

[ Client ]

fun main {

    val strategyA = ConcreteStrategyA()
    val strategyB = ConcreteStrategyB()
    val strategyC = ConcreteStrategyC()

    
    // 전략패턴 메서드 파라미터 주입 - 런타임 중 전략 알고리즘 변경에 유연하다
    fun stateStrategyTest() {
        val strategy = StrategyContext()

        // 악성 점수 +1 점짜리 상황
        strategy.execute(strategyA)
        
        // 악성 점수 +5 점짜리 상황
        strategy.execute(strategyB)
        
        // 악성 점수 *2 점짜리 상황
        strategy.execute(strategyC)
    }
}
  • Client 는 ConcreteStrategy와 Context를 통해 필요한 상황에 동적으로 알고리즘을 변경하여 적용할 수 있게 된다.

 

[ 참고용 Strategy 패턴 다이어그램 ]

 

 

 

 

 

 

 

 

 


 

[ A. 오늘 복습한 내용 / B. 다음에 학습할 내용 ]

A. Template method 패턴과 Strategy 패턴의 차이점

 

B. Factory method 패턴

 

B. Singleton 패턴

 

 


[Reference]

 

JAVA 언어로 배우는 디자인 패턴 입문: 쉽게 배우는 GoF의 23가지 디자인 패턴 | 유키 히로시 | 영진

▶ 3판에서 달라진 점 ㆍ예제 프로그램을 현대 Java 언어(람다식, 확장 for문, enum형 등)로 업데이트 ㆍ이해하기 쉽게 설명하되 현대의 관점에서 예제와 설명(의존성 주입, 보안 관련 설명 등) 수정

ebook-product.kyobobook.co.kr

 

 

Strategy

The Strategy pattern lets you isolate the code, internal data, and dependencies of various algorithms from the rest of the code. Various clients get a simple interface to execute the algorithms and switch them at runtime. The Strategy pattern lets you do a

refactoring.guru