봄맞이 세일

전략 패턴

다음 이름으로도 불립니다: Strategy

의도

전략 패턴은 알고리즘들의 패밀리를 정의하고, 각 패밀리를 별도의 클래스에 넣은 후 그들의 객체들을 상호교환할 수 있도록 하는 행동 디자인 패턴입니다.

전략 디자인 패턴

문제

어느 날 당신은 여행자들을 위한 내비 앱을 만들기로 했습니다. 앱의 중심 기능은 사용자들이 어느 도시에서든 빠르게 방향을 잡을 수 있도록 도와주는 아름다운 지도였습니다.

앱에서 가장 많이 요청된 기능 중 하나는 자동 경로 계획 기능이었습니다. 사용자가 주소를 입력하면 지도에 표시된 해당 목적지로 가는 가장 빠른 경로를 볼 수 있는 기능이었죠.

앱의 첫 번째 버전에서는 도로로 된 경로만을 만들 수 있었습니다. 차를 타고 여행하는 사용자들은 만족했습니다. 하지만 모든 사용자가 여가 중에 운전하는 걸 좋아하진 않았습니다. 그래서 그다음 업데이트에서는 도보 경로를 만드는 옵션을 추가했습니다. 바로 그다음에는 사람들이 경로에서 대중교통의 사용을 계획할 수 있도록 옵션을 추가했습니다.

하지만 그것은 시작에 불과했습니다. 나중에는 자전거를 타는 사용자들을 위한 경로를 만들 계획을 세웠습니다. 심지어 그다음에는 도시의 모든 관광 명소들을 지나는 경로를 만들 수 있는 또 다른 옵션을 추가할 계획을 세웠습니다.

내비게이터의 코드가 매우 복잡해졌습니다.

내비게이터의 코드가 복잡해졌습니다.

사업적인 측면에서 앱은 성공했지만, 기술적인 문제들이 많은 골칫거리를 야기했습니다. 새 경로 구축 알고리즘을 추가할 때마다 내비게이터의 메인 클래스의 크기가 두 배로 늘어났으며, 어느 시점이 되자 내비 앱은 유지하기가 너무 어려워졌습니다.

간단한 버그를 수정하거나 주행거리 점수를 살짝 조정하기 위해 알고리즘 중 하나를 변경하면 전체 클래스에 영향이 미쳐 이미 작동하는 코드에서 오류가 발생할 가능성이 높아졌습니다.

또한 팀워크가 비효율적이 되었습니다. 앱 출시 직후 고용된 팀원들은 병합 충돌을 해결하는 데 너무 많은 시간을 할애해야 한다고 불평했습니다. 또 새로운 기능을 구현하려면 거대한 동일 클래스를 변경해야 했는데, 이렇게 바꾼 내용들이 다른 팀원들이 생성한 코드와 충돌하곤 했습니다.

해결책

전략 패턴은 특정 작업을 다양한 방식으로 수행하는 클래스를 선택한 후 모든 알고리즘을 (strategies)​이라는 별도의 클래스들로 추출할 것을 제안합니다.

(context)​라는 원래 클래스에는 전략 중 하나에 대한 참조를 저장하기 위한 필드가 있어야 합니다. 콘텍스트는 작업을 자체적으로 실행하는 대신 연결된 전략 객체에 위임합니다.

콘텍스트는 작업에 적합한 알고리즘을 선택할 책임이 없습니다. 대신 클라이언트가 원하는 전략을 콘텍스트에 전달합니다. 사실, 콘텍스트는 전략들에 대해 많이 알지 못합니다. 콘텍스트는 같은 일반 인터페이스를 통해 모든 전략과 함께 작동하며, 이 일반 인터페이스는 선택된 전략 내에 캡슐화된 알고리즘을 작동시킬 단일 메서드만 노출합니다.

이렇게 하면 콘텍스트가 구상 전략들에 의존하지 않게 되므로 콘텍스트 또는 다른 전략들의 코드를 변경하지 않고도 새 알고리즘들을 추가하거나 기존 알고리즘들을 수정할 수 있습니다.

경로 계획 전략들.

경로 계획 전략들.

당신의 내비 앱에서 각 경로 구축 알고리즘을 단일 build­Route 메서드를 사용하여 자체 클래스로 추출할 수 있습니다. 이 메서드는 출발지와 목적지를 받은 후 경로의 체크포인트들의 컬렉션을 반환합니다.

같은 인수가 주어졌더라도 각 경로 구축 클래스는 다른 경로를 구축할 수 있지만 주 내비게이터 클래스는 어떤 알고리즘이 선택되었는지 별로 신경 쓰지 않습니다. 왜냐하면 주 내비게이터 클래스의 주요 작업은 지도에 체크포인트들의 집합을 렌더링하는 것이기 때문입니다. 이 클래스에는 활성 경로 구축 전략을 전환하는 메서드가 있어, 클래스의 클라이언트들이 (예: 사용자 인터페이스의 버튼들) 현재 선택된 경로 구축 행동들을 다른 행동으로 대체할 수 있습니다.

실제상황 적용

다양한 운송 전략들

공항에 도착하기 위한 다양한 전략들.

공항에 가야 한다고 상상해 보세요. 당신은 버스를 탈 수도 있고, 택시나 자전거를 탈 수도 있습니다. 이것들이 바로 당신의 운송 전략들입니다. 예산이나 시간 제약 등을 고려하여 이러한 전략 중 하나를 선택할 수 있습니다.

구조

전략 디자인 패턴의 구조전략 디자인 패턴의 구조
  1. 콘텍스트는 구상 전략 중 하나에 대한 참조를 유지하고 전략 인터페이스를 통해서만 이 객체와 통신합니다.

  2. 전략 인터페이스는 모든 구상 전략에 공통이며, 콘텍스트가 전략을 실행하는 데 사용하는 메서드를 선언합니다.

  3. 구상 전략들은 콘텍스트가 사용하는 알고리즘의 다양한 변형들을 구현합니다.

  4. 콘텍스트는 알고리즘을 실행해야 할 때마다 연결된 전략 객체의 실행 메서드를 호출합니다. 콘텍스트는 알고리즘이 어떻게 실행되는지와 자신이 어떤 유형의 전략과 함께 작동하는지를 모릅니다.

  5. 클라이언트는 특정 전략 객체를 만들어 콘텍스트에 전달합니다. 콘텍스트는 클라이언트들이 런타임에 콘텍스트와 관련된 전략을 대체할 수 있도록 하는 세터​(setter)​를 노출합니다.

의사코드

이 예시에서의 콘텍스트는 여러 전략들을 사용하여 다양한 산술 연산들을 실행합니다.

// 전략 인터페이스는 어떤 알고리즘의 모든 지원 버전에 공통적인 작업을 선언합니다.
// 콘텍스트는 이 인터페이스를 사용하여 구상 전략들에 의해 정의된 알고리즘을
// 호출합니다.
interface Strategy is
    method execute(a, b)

// 구상 전략들은 기초 전략 인터페이스를 따르면서 알고리즘을 구현합니다. 인터페이스는
// 그들이 콘텍스트에서 상호교환할 수 있게 만듭니다.
class ConcreteStrategyAdd implements Strategy is
    method execute(a, b) is
        return a + b

class ConcreteStrategySubtract implements Strategy is
    method execute(a, b) is
        return a - b

class ConcreteStrategyMultiply implements Strategy is
    method execute(a, b) is
        return a * b

// 콘텍스트는 클라이언트들이 관심을 갖는 인터페이스를 정의합니다.
class Context is
    // 콘텍스트는 전략 객체 중 하나에 대한 참조를 유지합니다. 콘텍스트는 전략의
    // 구상 클래스를 알지 못하며, 전략 인터페이스를 통해 모든 전략과 함께
    // 작동해야 합니다.
    private strategy: Strategy

    // 일반적으로 콘텍스트는 생성자를 통해 전략을 수락하고 런타임에 전략이 전환될
    // 수 있도록 세터도 제공합니다.
    method setStrategy(Strategy strategy) is
        this.strategy = strategy

    // 콘텍스트는 자체적으로 여러 버전의 알고리즘을 구현하는 대신 일부 작업을 전략
    // 객체에 위임합니다.
    method executeStrategy(int a, int b) is
        return strategy.execute(a, b)


// 클라이언트 코드는 구상 전략을 선택하고 콘텍스트에 전달합니다. 클라이언트는 올바른
// 선택을 하기 위해 전략 간의 차이점을 알고 있어야 합니다.
class ExampleApplication is
    method main() is
        Create context object.

        Read first number.
        Read last number.
        Read the desired action from user input.

        if (action == addition) then
            context.setStrategy(new ConcreteStrategyAdd())

        if (action == subtraction) then
            context.setStrategy(new ConcreteStrategySubtract())

        if (action == multiplication) then
            context.setStrategy(new ConcreteStrategyMultiply())

        result = context.executeStrategy(First number, Second number)

        Print result.

적용

전략 패턴은 객체 내에서 한 알고리즘의 다양한 변형들을 사용하고 싶을 때, 그리고 런타임 중에 한 알고리즘에서 다른 알고리즘으로 전환하고 싶을 때 사용하세요.

또 전략 패턴은 객체의 행동들을 특정 하위 행동들을 다양한 방식으로 수행할 수 있는 다른 하위 객체들과 연관시켜 객체의 행동들을 런타임에 간접적으로 변경할 수 있게 해줍니다.

전략 패턴은 일부 행동을 실행하는 방식에서만 차이가 있는 유사한 클래스들이 많은 경우에 사용하세요.

전략 패턴은 다양한 행동들을 별도의 클래스 계층구조로 추출하고 원래 클래스들을 하나로 결합하여 중복 코드를 줄일 수 있게 해줍니다.

전략 패턴을 사용하여 클래스의 비즈니스 로직을 해당 로직의 콘텍스트에서 그리 중요하지 않을지도 모르는 알고리즘들의 구현 세부 사항들로부터 고립하세요.

전략 패턴은 코드의 나머지 부분에서 해당 코드, 내부 데이터, 그리고 다양한 알고리즘들의 의존 관계들을 고립시킬 수 있습니다. 다양한 클라이언트들이 알고리즘들을 실행하고 런타임에 전환하기 위한 간단한 인터페이스를 얻습니다.

이 패턴은 같은 알고리즘의 다른 변형들 사이를 전환하는 거대한 조건문이 당신의 클래스에 있는 경우에 사용하세요.

전략 패턴을 사용하면 모든 알고리즘을 같은 인터페이스를 구현하는 별도의 클래스들로 추출하여 이러한 조건문을 제거할 수 있습니다. 원래 객체는 알고리즘의 모든 변형들을 구현하는 대신 이러한 객체들 중 하나에 실행을 위임합니다.

구현방법

  1. 콘텍스트 클래스에서 자주 변경되는 알고리즘을 식별하세요. 런타임에 같은 알고리즘의 변형을 선택한 후 실행하는 거대한 조건문일 수도 있습니다.

  2. 알고리즘의 모든 변형에 공통인 전략 인터페이스를 선언하세요.

  3. 하나씩 모든 알고리즘을 자체 클래스들로 추출하세요. 그들은 모두 전략 인터페이스를 구현해야 합니다.

  4. 콘텍스트 클래스에서 전략 객체에 대한 참조를 저장하기 위한 필드를 추가한 후, 해당 필드의 값을 대체하기 위한 세터를 제공하세요. 콘텍스트는 전략 인터페이스를 통해서만 전략 객체와 작동해야 합니다. 콘텍스트는 인터페이스를 정의할 수 있으며, 이 인터페이스는 전략이 콘텍스트의 데이터에 접근할 수 있도록 합니다.

  5. 콘텍스트의 클라이언트들은 콘텍스트를 적절한 전략과 연관시켜야 합니다. 이러한 전략은 클라이언트들이 기대하는 콘텍스트가 주 작업을 수행하는 방식과 일치해야 합니다.

장단점

  • 런타임에 한 객체 내부에서 사용되는 알고리즘들을 교환할 수 있습니다.
  • 알고리즘을 사용하는 코드에서 알고리즘의 구현 세부 정보들을 고립할 수 있습니다.
  • 상속을 합성으로 대체할 수 있습니다.
  • / . 콘텍스트를 변경하지 않고도 새로운 전략들을 도입할 수 있습니다.
  • 알고리즘이 몇 개밖에 되지 않고 거의 변하지 않는다면, 패턴과 함께 사용되는 새로운 클래스들과 인터페이스들로 프로그램을 지나치게 복잡하게 만들 이유가 없습니다.
  • 클라이언트들은 적절한 전략을 선택할 수 있도록 전략 간의 차이점들을 알고 있어야 합니다.
  • 현대의 많은 프로그래밍 언어에는 익명 함수들의 집합 내에서 알고리즘의 다양한 버전들을 구현할 수 있는 함수형 지원이 있으며, 클래스들과 인터페이스들을 추가하여 코드의 부피를 늘리지 않으면서도 전략 객체를 사용했을 때와 똑같이 이러한 함수들을 사용할 수 있습니다.

다른 패턴과의 관계

  • 브리지, 상태, 전략 패턴은 매우 유사한 구조로 되어 있으며, 어댑터 패턴도 이들과 어느 정도 유사한 구조로 되어 있습니다. 위 모든 패턴은 다른 객체에 작업을 위임하는 합성을 기반으로 합니다. 하지만 이 패턴들은 모두 다른 문제들을 해결합니다. 패턴은 특정 방식으로 코드의 구조를 짜는 레시피에 불과하지 않습니다. 왜냐하면 패턴은 해결하는 문제를 다른 개발자들에게 전달할 수도 있기 때문입니다.

  • 커맨드전략 패턴은 비슷해 보일 수 있습니다. 왜냐하면 둘 다 어떤 작업으로 객체를 매개변수화하는 데 사용할 수 있기 때문입니다. 그러나 이 둘의 의도는 매우 다릅니다.

    • 당신은 를 사용하여 모든 작업을 객체로 변환할 수 있습니다. 작업의 매개변수들은 해당 객체의 필드들이 됩니다. 이 변환은 작업의 실행을 연기하고, 해당 작업을 대기열에 넣고, 커맨드들의 기록을 저장한 후 해당 커맨드들을 원격 서비스에 보내는 등의 작업을 가능하게 합니다.

    • 반면에 은 일반적으로 같은 작업을 수행하는 다양한 방법을 설명하므로 단일 콘텍스트 클래스 내에서 이러한 알고리즘들을 교환할 수 있도록 합니다.

  • 데코레이터는 객체의 피부를 변경할 수 있고 전략 패턴은 객체의 내장을 변경할 수 있다고 비유할 수 있습니다.

  • 템플릿 메서드는 상속을 기반으로 합니다. 이 메서드는 자식 클래스들에서 알고리즘의 부분들을 확장하여 변경할 수 있도록 합니다. 전략 패턴은 합성을 기반으로 합니다: 당신은 객체 행동의 일부분들을 이러한 행동에 해당하는 다양한 전략들을 제공하여 변경할 수 있습니다. 릿 는 클래스 수준에서 작동하므로 정적입니다. 은 객체 수준에서 작동하므로 런타임에 행동들을 전환할 수 있도록 합니다.

  • 상태전략의 확장으로 간주할 수 있습니다. 두 패턴 모두 합성을 기반으로 합니다. 그들은 어떤 작업을 도우미 객체들에 전달하여 콘텍스트의 행동을 바꿉니다. 은 이러한 객체들을 완전히 독립적으로 만들어 서로를 인식하지 못하도록 만듭니다. 그러나 는 구상 상태들 사이의 의존 관계들을 제한하지 않으므로 그들이 콘텍스트의 상태를 마음대로 변경할 수 있도록 합니다.

코드 예시

C#으로 작성된 전략 C++로 작성된 전략 Go로 작성된 전략 자바로 작성된 전략 PHP로 작성된 전략 파이썬으로 작성된 전략 루비로 작성된 전략 러스트로 작성된 전략 스위프트로 작성된 전략 타입스크립트로 작성된 전략