디자인 패턴(1)

주상돈·2025년 2월 27일

TIL

목록 보기
35/53
post-thumbnail

전략패턴


특정한 작업(알고리즘)을 독립적으로 정의하고 캡슐화 하여, 해당 작업을 동적으로 교체할 수 있도록 하는 패턴을 의미한다.

우리가 다양한 오리를 구현해야 한다고 가정해보자.

  • Duck이라는 클래스를 작성한 다음, 아래와 같이 여러 오리들이 해당 클래스를 상속하도록 할 수 있다.

우리는 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리해야 한다.

오리의 예제에서 달라지는 부분과 달라지지 않는 부분을 분리하면 아래와 같다.

꽥꽥 우는 행동이나, 나는 행동은 오리 종류에 따라 달라지는 부분으로, Duck 클래스에서 분리해야 한다.

다시 말해, 해당 행동들을 Duck 클래스에서 직접 구현하지 않아야 한다. 그렇다면 해당 행동들은 어떻게 처리해야 할까?


인터페이스(QuackBehavior, FlyBehavior)를 사용하면 된다.

그리고 각각의 인터페이스를 Duck 클래스에서 구현하는 것이 아니라 아닌 특정 행동 만을 목적으로 하는 클래스에서 구현하도록 하자.

아래처럼 quak( ) 메서드와 fly( ) 메서드는 Duck 클래스가 아닌 특정 행동 인터페이스를 구현한 별도의 클래스 안에 존재하게 된다.


1. 행동 인터페이스 정의

// 행동 인터페이스
public interface IFlyBehavior
{
    void Fly();
}

public interface IQuackBehavior
{
    void Quack();
}
  1. 인터페이스 기반 구체적인 행동 구현
// 날 수 있는 행동 구현
public class FlyWithWings : IFlyBehavior
{
    public void Fly()
    {
        Console.WriteLine("날개로 납니다.");
    }
}

// 날지 못하는 행동 구현
public class FlyNoWay : IFlyBehavior
{
    public void Fly()
    {
        Console.WriteLine("날지 못합니다.");
    }
}

public class FlyRocketPowered : IFlyBehavior
{
    public void Fly()
    {
        Console.WriteLine("로켓 추진으로 엄청나게 날아갑니다!!!");
    }
}

// 꽥꽥 소리를 내는 행동 구현
public class Quack : IQuackBehavior
{
    public void Quack()
    {
        Console.WriteLine("꽥꽥 소리를 냅니다.");
    }
}

// 삑삑 소리를 내는 행동 구현
public class Squeak : IQuackBehavior
{
    public void Quack()
    {
        Console.WriteLine("삑삑 소리를 냅니다.");
    }
}

public class MuteQuack : IQuackBehavior
{
    public void Quack()
    {
        Console.WriteLine("... (침묵)");
    }
}
  1. 인터페이스를 포함하는 클래스 정의
public abstract class Duck
{
    protected IFlyBehavior flyBehavior;
    protected IQuackBehavior quackBehavior;

    public Duck()
    {
    }

    public abstract void Display();

    public void PerformFly()
    {
        flyBehavior.Fly();
    }

    public void PerformQuack()
    {
        quackBehavior.Quack();
    }

    public void Swim()
    {
        Console.WriteLine("모든 오리는 물에 뜹니다.");
    }
    
    public void SetFlyBehavior(IFlyBehavior fb)
    {
        flyBehavior = fb;
    }
    
     public void SetQuackBehavior(IQuackBehavior qb)
    {
        quackBehavior = qb;
    }
}
  1. 구체적인 클래스 정의
public class MallardDuck : Duck
{
    public MallardDuck()
    {
        flyBehavior = new FlyWithWings();
        quackBehavior = new Quack();
    }

    public override void Display()
    {
        Console.WriteLine("Mallard 오리");
    }
}

public class RubberDuck : Duck
{
    public RubberDuck()
    {
        flyBehavior = new FlyNoWay();
        quackBehavior = new Squeak();
    }

    public override void Display()
    {
        Console.WriteLine("Rubber 오리");
    }
}
  1. 실제 사용
class Program
{
    static void Main(string[] args)
    {
        Duck mallardDuck = new MallardDuck();
        Duck rubberDuck = new RubberDuck();

        mallardDuck.Display();
        mallardDuck.PerformFly();
        mallardDuck.PerformQuack();

        rubberDuck.Display();
        rubberDuck.SetFlyBehavior(new FlyRocketPowered());
        rubberDuck.PerformFly();
        rubberDuck.PerformQuack();
    }
}

여기서 핵심은 실제 실행 시에 사용되는 객체(MallardDuck, RubberDuck)가 상위 형식(Duck)으로 만들어 질 수 있다는 것 이다.

Main 함수에서는 MallardDuck이나 RubberDuck의 구체적인 행동을 알 수 없다.

그냥 Duck 클래스의 PerformFly( ), PerformQuack( ) 메서드를 호출할 뿐이다.

이렇게 구현하면 무엇이 좋은 것일까? 앞선 상황을 다시 생각해보자.

나무 오리를 새롭게 만든다고 하면, 꽥꽥 울 수도, 날 수도 없다.

  1. 나무 오리 클래스 정의
public class DecoyDuck : Duck
{
    public DecoyDuck()
    {
        flyBehavior = new FlyNoWay(); // 날지 못하는 행동 설정
        quackBehavior = new MuteQuack(); // 소리를 내지 않는 행동 설정
    }

    public override void Display()
    {
        Console.WriteLine("나무 오리");
    }
}
  1. 새로운 행동 구현
// 소리를 내지 않는 행동 구현
public class MuteQuack : IQuackBehavior
{
    public void Quack()
    {
        Console.WriteLine("소리를 내지 않습니다.");
    }
}
  1. 실제 사용
class Program
{
    static void Main(string[] args)
    {
        Duck mallardDuck = new MallardDuck();
        Duck rubberDuck = new RubberDuck();
        Duck decoyDuck = new DecoyDuck();

        mallardDuck.Display();
        mallardDuck.PerformFly();
        mallardDuck.PerformQuack();

        rubberDuck.Display();
        rubberDuck.PerformFly();
        rubberDuck.PerformQuack();

        decoyDuck.Display();
        decoyDuck.PerformFly();
        decoyDuck.SetQuackBehvior(new Squeak ());
        decoyDuck.PerformQuack();
    }
}

이렇게 달라지는 부분을 캡슐화하고, 동적으로 교체되어 사용할 수 있도록 한다면(전략 패턴을 사용한다면)

상속으로 발생하는 예상치 못한 영향과, 불필요한 코드 작성을 피할 수 있게 된다.

전략 패턴 장단점 및 활용 방안


  • 장점
    • 유연성: 새로운 행동 클래스만 추가하면 되므로, 기존 코드 영향이 적다.
    • 결합도 감소: Duck은 IFlyBehavior, IQuackBehavior 인터페이스만 알면 되므로, 구체 구현 변경 시 수정 범위가 최소화된다.
    • 런타임 교체: SetFlyBehavior, SetQuackBehavior 등을 통해 실행 중에도 행동을 교체할 수 있다.
  • 단점
    • 클래스 증가: 행동마다 별도 클래스를 만들어야 하므로, 파일이 늘어날 수 있다.
    • 행동 설정: Duck 생성자 혹은 팩토리에서 어떤 행동을 쓸지 결정하는 과정이 필요하다.
  • 활용 방안
    • 자주 바뀌는 로직(할인 정책, 무기 시스템, 정렬 알고리즘 등)에 대해서 전략 패턴을 적용하시면 유리하다.
    • 복잡한 if-else 또는 switch문으로 분기 처리하던 로직을 “행동” 객체로 분리함으로써 코드 가독성과 유지보수성을 높일 수 있다.

0개의 댓글