전략 패턴

Jinho Lee·2025년 2월 18일

설명

  • 전략 패턴(strategy pattern)은 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다.

  • 전략 패턴은

    • 특정한 계열의 알고리즘들을 정의하고

    • 각 알고리즘을 캡슐화하며

    • 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다.

  • 객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고 전략을 바꿔주기만 함으로써 행위를 유연하게 확장한다.

장단점과 사용 시기

  • 장점

    1. 개방 폐쇄 원칙(OCP) 준수 : 콘텍스트(클라이언트)의 기능을 수정하지 않고도 새로운 전략(기능)을 도입할 수 있다.

    2. 런타임에 한 객체 내부에서 사용되는 알고리즘들을 동적으로 교환할 수 있다.

    3. 상속을 컴포지션(Composition)으로 대체할 수 있다.

      • 컴포지션 : 클래스를 구성하는 부분의 합. 한 클래스가 다른 클래스의 구성요소가 되는 것.
  • 단점

    1. 알고리즘이 몇 개밖에 되지 않고 변하지 않는다면, 괜히 새로운 클래스와 인터페이스들로 코드를 복잡하게 만들 필요가 없다.

    2. 클라이언트들이 적절한 전략을 선택할 수 있도록 전략 간의 차이점을 알고 있어야 한다.

    3. 현대에는 익명 함수들의 집합 안에서 알고리즘의 다양한 버전을 구현할 수 있는 함수형 지원이 있어, 코드의 양을 늘리지 않으면서도 전략 패턴을 사용했을 때와 같이 함수를 구현해 사용할 수 있다.

  • 사용 시기

    • 객체 안에서 한 알고리즘의 다양한 변형을 사용하고 싶을 때

    • 런타임 중에 동적으로 한 알고리즘에서 다른 알고리즘으로 전환하고 싶을 때

    • 일부 행동(기능)을 실행하는 방식에서 차이가 있는 유사한 클래스들이 많을 때

    • 같은 알고리즘의 다른 변형들 사이를 전환하는 거대한 조건문이 클래스에 있을 때

구조

  • Context : Strategy를 이용하는 시스템. 전략 알고리즘을 직접 구현하지 않는다. 대신, strategy.algorithm() 알고리즘을 구현하는 Strategy를 참조한다.

  • Strategy 인터페이스 : 전략을 구현하여 시스템에 제공한다. 이로써 Context을 알고리즘의 구현에서 독립시킨다.

  • Strategy1, Strategy2 : 구체적인 알고리즘의 구현. 인터페이스를 구현해 만들어진 전략.

구현 및 예시

구현 방법

  1. Context는 전략 중 하나에 대한 참조를 유지하고 전략 인터페이스를 통해서만 이 객체와 통신한다.

  2. Strategy 인터페이스는 모든 전략에 공통이며, Context가 전략을 실행하는 데 사용하는 메소드를 선언한다.

  3. ConcreteStrategy들은 Context가 사용하는 알고리즘의 다양한 변형들을 구현한다.

  4. Context는 알고리즘을 실행할 때마다 연결된 전략 객체의 메소드를 호출한다. 여기서 Context는 알고리즘이 어떻게 실행되는지, 어떤 유형의 전략을 작동하는지 모른다.

  5. Client는 특정 전략 객체를 만들어 Context에 전달한다. 이 때 ContextClient들이 런타임에 전략을 대체할 수 있도록 하는 세터Setter를 노출한다.

예시와 함께 보는 전략 패턴 구현 방법

  1. 전략을 생성한다.

    • 운송 수단은 선로를 따라 움직이든지, 도로를 따라 움직이든지 두 가지 방법을 Strategy 클래스를 생성하는 것으로 구현한다.

    • 그리고 두 클래스는 move() 메소드를 구현한다.

    • 두 전략 클래스를 IMovableStrategy 인터페이스로 캡슐화하여 다른 전략이 추가로 확장되는 경우를 고려해 설계하자.

public interface IMovableStrategy
{
	public void move();
}

public class RailLoadStrategy : IMovableStrategy
{
	public void move()
    {
    	Debug.Log("선로를 통해 이동");
    }
}

public class LoadStrategy : IMovableStrategy
{
	public void move()
    {
    	Debug.Log("도로를 통해 이동");
    }
}
  1. 운송 수단에 대한 클래스를 정의한다.

    • 운송 수단은 move() 메소드를 통해 움직인다.

    • 이동 방식을 전략을 통해 정한다.

    • 이때 전략을 정할 수 있는 메소드 setMovableStrategy를 구현한다.

public class Moving
{
    private MovableStrategy movableStrategy;

    public void move()
    {
        movableStrategy.move();
    }

    public void setMovableStrategy(MovableStrategy movableStrategy)
    {
        this.movableStrategy = movableStrategy;
    }
}

public class Bus : Moving
{
	
}

public class Train : Moving
{
	
}
  1. 운송 수단 객체를 사용하는 Client 객체를 구현한다.

    • TrainBus 객체를 생성하고, 각 운송 수단의 이동 방식을 setMovableStrategy() 메소들르 호출하는 것으로 정한다.

    • 여기서 Bus의 이동 방식을 유연하게 수정할 수 있다.

public class Client
{
    public int main()
    {
        Moving train = new Train();
        Moving bus = new Bus();

        /*
            기존의 기차와 버스의 이동 방식
            1) 기차 - 선로
            2) 버스 - 도로
         */
        train.setMovableStrategy(new RailLoadStrategy());
        bus.setMovableStrategy(new LoadStrategy());

        train.move();	// 선로를 통해 이동
        bus.move();		// 도로를 통해 이동

        /*
            선로를 따라 움직이는 버스가 개발
         */
        bus.setMovableStrategy(new RailLoadStrategy());
        bus.move();		// 선로를 통해 이동
    }
}

의사코드

  • 이 예시에서의 Context는 여러 전략들을 사용하여 다양한 산술 연산들을 실행한다.
// 전략 인터페이스는 어떤 알고리즘의 모든 지원 버전에 공통적인 작업을 선언합니다.
// 콘텍스트는 이 인터페이스를 사용하여 구상 전략들에 의해 정의된 알고리즘을
// 호출합니다.
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.

C# 예시 코드 1

using System;
using System.Collections.Generic;

namespace RefactoringGuru.DesignPatterns.Strategy.Conceptual
{
    // The Context defines the interface of interest to clients.
    class Context
    {
        // The Context maintains a reference to one of the Strategy objects. The
        // Context does not know the concrete class of a strategy. It should
        // work with all strategies via the Strategy interface.
        private IStrategy _strategy;

        public Context()
        { }

        // Usually, the Context accepts a strategy through the constructor, but
        // also provides a setter to change it at runtime.
        public Context(IStrategy strategy)
        {
            this._strategy = strategy;
        }

        // Usually, the Context allows replacing a Strategy object at runtime.
        public void SetStrategy(IStrategy strategy)
        {
            this._strategy = strategy;
        }

        // The Context delegates some work to the Strategy object instead of
        // implementing multiple versions of the algorithm on its own.
        public void DoSomeBusinessLogic()
        {
            Console.WriteLine("Context: Sorting data using the strategy (not sure how it'll do it)");
            var result = this._strategy.DoAlgorithm(new List<string> { "a", "b", "c", "d", "e" });

            string resultStr = string.Empty;
            foreach (var element in result as List<string>)
            {
                resultStr += element + ",";
            }

            Console.WriteLine(resultStr);
        }
    }

    // The Strategy interface declares operations common to all supported
    // versions of some algorithm.
    //
    // The Context uses this interface to call the algorithm defined by Concrete
    // Strategies.
    public interface IStrategy
    {
        object DoAlgorithm(object data);
    }

    // Concrete Strategies implement the algorithm while following the base
    // Strategy interface. The interface makes them interchangeable in the
    // Context.
    class ConcreteStrategyA : IStrategy
    {
        public object DoAlgorithm(object data)
        {
            var list = data as List<string>;
            list.Sort();

            return list;
        }
    }

    class ConcreteStrategyB : IStrategy
    {
        public object DoAlgorithm(object data)
        {
            var list = data as List<string>;
            list.Sort();
            list.Reverse();

            return list;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // The client code picks a concrete strategy and passes it to the
            // context. The client should be aware of the differences between
            // strategies in order to make the right choice.
            var context = new Context();

            Console.WriteLine("Client: Strategy is set to normal sorting.");
            context.SetStrategy(new ConcreteStrategyA());
            context.DoSomeBusinessLogic();
            
            Console.WriteLine();
            
            Console.WriteLine("Client: Strategy is set to reverse sorting.");
            context.SetStrategy(new ConcreteStrategyB());
            context.DoSomeBusinessLogic();
        }
    }
}
  • 실행 결과
Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
a,b,c,d,e

Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
e,d,c,b,a

C# 예시 코드 2

public class StrategyPatternWiki
{
    public static void Main(String[] args)
    {
        Customer firstCustomer = new Customer(new NormalStrategy());

        // Normal billing
        firstCustomer.Add(1.0, 1);

        // Start Happy Hour
        firstCustomer.Strategy = new HappyHourStrategy();
        firstCustomer.Add(1.0, 2);

        // New Customer
        Customer secondCustomer = new Customer(new HappyHourStrategy());
        secondCustomer.Add(0.8, 1);
        // The Customer pays
        firstCustomer.PrintBill();

        // End Happy Hour
        secondCustomer.Strategy = new NormalStrategy();
        secondCustomer.Add(1.3, 2);
        secondCustomer.Add(2.5, 1);
        secondCustomer.PrintBill();
    }
}


class Customer
{
    private IList<double> drinks;

    // Get/Set Strategy
    public IBillingStrategy Strategy { get; set; }

    public Customer(IBillingStrategy strategy)
    {
        this.drinks = new List<double>();
        this.Strategy = strategy;
    }

    public void Add(double price, int quantity)
    {
        drinks.Add(Strategy.GetActPrice(price * quantity));
    }

    // Payment of bill
    public void PrintBill()
    {
        double sum = 0;
        foreach (double i in drinks)
        {
            sum += i;
        }
        Console.WriteLine("Total due: " + sum);
        drinks.Clear();
    }
}

interface IBillingStrategy
{
    double GetActPrice(double rawPrice);
}

// Normal billing strategy (unchanged price)
class NormalStrategy : IBillingStrategy
{
    public double GetActPrice(double rawPrice)
    {
        return rawPrice;
    }

}

// Strategy for Happy hour (50% discount)
class HappyHourStrategy : IBillingStrategy
{

    public double GetActPrice(double rawPrice)
    {
        return rawPrice * 0.5;
    }
}

참고

0개의 댓글