상태 패턴

Jinho Lee·2025년 2월 17일
0
post-thumbnail

설명

  • 상태 패턴(state pattern)객체 지향 방식으로 상태 기계를 구현하는 디자인 패턴이다.

  • 객체가 상태에 따라 행위를 다르게 할 때, 직접 상태를 체크하여 상태에 따른 행위를 호출하는 것이 아니라 상태를 객체화하여 필요에 따라 다르게 행동하도록 위임한다.

  • 유한 상태 기계(Finite State Machine, FSM)는 상태가 추가될 때마다 조건이 붙어 상태 전이를 위한 로직이 점점 복잡해진다. 상태 패턴은 각 상태에 대응하는 클래스를 만드는 것으로 이 복잡함을 해소할 수 있다.

    • 각 상태를 이용하는 호스트 객체를 컨텍스트(Context)라고도 부른다.
  • 상태 패턴을 사용하는 것으로 상태를 추가하거나 상태 전이 로직을 이해하기 쉽게 만드는 등, 유지보수에 이득을 얻을 수 있다.

  • GoF에 따르면 TCP connection 구현에 이 패턴이 사용되었다고 한다.

장단점과 사용 시기

  • 장점

    1. 단일 책임 원칙(SRP) 준수 : 특정 상태에 대한 코드를 별도 클래스로 구성

    2. 개방 폐쇄 원칙(OCP) 준수 : 기존 State 클래스 혹은 컨텍스트를 수정하지 않고 새로운 상태를 추가 가능

    3. 상태에 따른 동작을 각각의 클래스로 분산하여 코드 복잡도 감소

  • 단점

    1. 관리할 클래스 수 증가

    2. 객체에 적용할 상태가 몇가지 밖에 없거나 거의 상태 변경이 이루어지지 않는 경우 패턴을 적용하는 것이 과도할 수 있다.

  • 사용 시기

    • 객체의 기능이 상태에 따라 각기 다를 때

    • 상태 변경에 대한 조건 분기 코드가 많거나 중복될 때

    • 런타임에서 객체의 상태를 변경해야 할 때

구조

  • Context : State를 이용하는 시스템. 실제 행위를 실행하는 대신 상태에 위임한다.

  • State 인터페이스 : 시스템의 모든 상태에 공통의 인터페이스를 제공한다.

  • State1, State2 : 구체적인 각각의 상태 객체. 인터페이스를 구체적으로 구현한다.

구현 및 예시

구현 방법

  1. 컨텍스트 클래스를 정한다. 유한 상태 기계일 수도 있고, 조건문으로 상태를 관리하는 클래스일 수도 있고, 아예 새로운 클래스일 수도 있다.

  2. 상태 인터페이스를 선언한다. 상태별 동작을 포함하는 메소드만을 갖도록 한다.

  3. 모든 상태에 대해 상태 인터페이스에서 파생된 상태 클래스를 만든다. 컨텍스트에서 상태와 관련된 모든 코드를 추출해 넣는다.

  4. 컨텍스트 클래스에서 상태 인터페이스 유형의 참조 필드와 필드 값을 오버라이드할 수 있는 공개된 세터(setter)를 추가한다.

  5. 컨텍스트의 메소드를 보고 조건문을 상태 객체의 메소드 호출로 대체한다.

  6. 상태 클래스의 인스턴스를 컨텍스트에 전달하는 것으로 컨텍스트의 상태를 전환할 수 있다.

의사코드

// AudioPlayer(오디오 플레이어) 클래스는 콘텍스트 역할을 합니다. 이 클래스는 또
// 오디오 플레이어의 현재 상태를 나타내는 상태 클래스 중 하나의 인스턴스에 대한
// 참조를 유지합니다.
class AudioPlayer is
    field state: State
    field UI, volume, playlist, currentSong

    constructor AudioPlayer() is
        this.state = new ReadyState(this)

        // 콘텍스트는 사용자 입력 처리를 상태 객체에 위임합니다. 당연히 결과는
        // 현재 활성화된 상태에 따라 달라집니다. 왜냐하면 각 상태는 입력을
        // 다르게 처리할 수 있기 때문입니다.
        UI = new UserInterface()
        UI.lockButton.onClick(this.clickLock)
        UI.playButton.onClick(this.clickPlay)
        UI.nextButton.onClick(this.clickNext)
        UI.prevButton.onClick(this.clickPrevious)

    // 다른 객체들은 오디오 플레이어의 활성 상태를 전환할 수 있어야 합니다.
    method changeState(state: State) is
        this.state = state

    // 사용자 인터페이스 메서드들은 실행을 활성 상태에 위임합니다.
    method clickLock() is
        state.clickLock()
    method clickPlay() is
        state.clickPlay()
    method clickNext() is
        state.clickNext()
    method clickPrevious() is
        state.clickPrevious()

    // 상태는 콘텍스트에 일부 서비스 메서드들을 호출할 수 있습니다.
    method startPlayback() is
        // …
    method stopPlayback() is
        // …
    method nextSong() is
        // …
    method previousSong() is
        // …
    method fastForward(time) is
        // …
    method rewind(time) is
        // …


// 기초 상태 클래스는 모든 구상 상태들이 구현해야 하는 메서드들을 선언하고 상태와
// 연결된 콘텍스트 객체에 대한 역참조도 제공합니다. 상태는 역참조를 사용하여
// 콘텍스트를 다른 상태로 천이할 수 있습니다.
abstract class State is
    protected field player: AudioPlayer

    // 콘텍스트는 상태 생성자를 통해 자신을 전달합니다. 이는 필요한 경우 상태가
    // 유용한 콘텍스트 데이터를 가져오는 데 도움이 될 수 있습니다.
    constructor State(player) is
        this.player = player

    abstract method clickLock()
    abstract method clickPlay()
    abstract method clickNext()
    abstract method clickPrevious()


// 구상 상태들은 콘텍스트의 상태와 연관된 다양한 행동들을 구현합니다.
class LockedState extends State is

    // 잠긴 플레이어의 잠금을 해제하면 플레이어가 두 가지 상태 중 하나를 택할 수
    // 있습니다.
    method clickLock() is
        if (player.playing)
            player.changeState(new PlayingState(player))
        else
            player.changeState(new ReadyState(player))

    method clickPlay() is
        // 잠금 상태: 아무것도 하지 않는다.

    method clickNext() is
        // 잠금 상태: 아무것도 하지 않는다.

    method clickPrevious() is
        // 잠금 상태: 아무것도 하지 않는다.


// 콘텍스트에서 상태 천이를 실행시킬 수도 있습니다.
class ReadyState extends State is
    method clickLock() is
        player.changeState(new LockedState(player))

    method clickPlay() is
        player.startPlayback()
        player.changeState(new PlayingState(player))

    method clickNext() is
        player.nextSong()

    method clickPrevious() is
        player.previousSong()


class PlayingState extends State is
    method clickLock() is
        player.changeState(new LockedState(player))

    method clickPlay() is
        player.stopPlayback()
        player.changeState(new ReadyState(player))

    method clickNext() is
        if (event.doubleclick)
            player.nextSong()
        else
            player.fastForward(5)

    method clickPrevious() is
        if (event.doubleclick)
            player.previous()
        else
            player.rewind(5)

C# 예시 코드

using System;

namespace RefactoringGuru.DesignPatterns.State.Conceptual
{
    // The Context defines the interface of interest to clients. It also
    // maintains a reference to an instance of a State subclass, which
    // represents the current state of the Context.
    class Context
    {
        // A reference to the current state of the Context.
        private State _state = null;

        public Context(State state)
        {
            this.TransitionTo(state);
        }

        // The Context allows changing the State object at runtime.
        public void TransitionTo(State state)
        {
            Console.WriteLine($"Context: Transition to {state.GetType().Name}.");
            this._state = state;
            this._state.SetContext(this);
        }

        // The Context delegates part of its behavior to the current State
        // object.
        public void Request1()
        {
            this._state.Handle1();
        }

        public void Request2()
        {
            this._state.Handle2();
        }
    }
    
    // The base State class declares methods that all Concrete State should
    // implement and also provides a backreference to the Context object,
    // associated with the State. This backreference can be used by States to
    // transition the Context to another State.
    abstract class State
    {
        protected Context _context;

        public void SetContext(Context context)
        {
            this._context = context;
        }

        public abstract void Handle1();

        public abstract void Handle2();
    }

    // Concrete States implement various behaviors, associated with a state of
    // the Context.
    class ConcreteStateA : State
    {
        public override void Handle1()
        {
            Console.WriteLine("ConcreteStateA handles request1.");
            Console.WriteLine("ConcreteStateA wants to change the state of the context.");
            this._context.TransitionTo(new ConcreteStateB());
        }

        public override void Handle2()
        {
            Console.WriteLine("ConcreteStateA handles request2.");
        }
    }

    class ConcreteStateB : State
    {
        public override void Handle1()
        {
            Console.Write("ConcreteStateB handles request1.");
        }

        public override void Handle2()
        {
            Console.WriteLine("ConcreteStateB handles request2.");
            Console.WriteLine("ConcreteStateB wants to change the state of the context.");
            this._context.TransitionTo(new ConcreteStateA());
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // The client code.
            var context = new Context(new ConcreteStateA());
            context.Request1();
            context.Request2();
        }
    }
}
  • 실행 결과
Context: Transition to ConcreteStateA.
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to ConcreteStateB.
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to ConcreteStateA.

예시를 통한 구현 방법 설명

  • 간단한 티켓 자판기를 만들어 보자. 이 자판기는 두 개의 상태를 가진다.
    • 동전이 없는 상태
      • 티켓을 출력하려 해도 상태가 변하지 않는다.
      • 동전을 넣으면 동전이 있는 상태로 변한다.
    • 동전이 있는 상태
      • 동전을 더 넣어도 상태는 변하지 않는다.
      • 티켓을 뽑으면 동전이 없는 상태로 변한다.
  • DFA
  1. 컨텍스트 클래스를 정한다.

    public class TicketMachine
    {
        private bool isCoin = false;
    
        private void insertCoin()
        {
            if (isCoin == false)
            {
                isCoin = true;
                Console.WriteLine("동전을 넣었습니다.");
            }
            else
                Console.WriteLine("이미 동전이 들어있습니다.");
        }
    
        private void printTicket()
        {
            if (isCoin == true)
            {
                isCoin = false;
                Console.WriteLine("티켓을 뽑았습니다.");
            }
            else
                Console.WriteLine("동전이 없습니다. 동전을 넣어주세요.");
        }
    
        public void input()
        {
            if (Console.ReadLine() == "coin")
                insertCoin();
            else
                printTicket();
        }
    }
  2. 상태 인터페이스를 선언한다.

    public interface IState
    {
        void insertCoin(TicketMachine ticketMachine);
        void printTicket(TicketMachine ticketMachine);
    }
  3. 모든 상태에 대해 상태 인터페이스에서 파생된 상태 클래스를 만든다.

    public class CoinState : IState
    {
        public void insertCoin(TicketMachine ticketMachine)
        {
            Console.WriteLine("이미 동전이 들어있습니다.");
            
        }
    
        public void printTicket(TicketMachine ticketMachine)
        {
            Console.WriteLine("티켓을 뽑았습니다.");
            ticketMachine.SetState(new NoCoinState());
        }
    }
    
    public class NoCoinState : IState
    {
        public void insertCoin(TicketMachine ticketMachine)
        {
            Console.WriteLine("동전을 넣었습니다.");
            ticketMachine.SetState(new CoinState());
        }
    
        public void printTicket(TicketMachine ticketMachine)
        {
            Console.WriteLine("동전이 없습니다. 동전을 넣어주세요.");
        }
    }
  4. 컨텍스트 클래스에서 상태 인터페이스 유형의 참조 필드와 필드 값을 오버라이드할 수 있는 공개된 세터(setter)를 추가한다.

    public class TicketMachine
    {
    	private IState currentState = new NoCoinState();
      
      	public void SetState(IState state)
      	{
      		currentState = state;
      	}
       	...
  5. 컨텍스트의 메소드를 보고 조건문을 상태 객체의 메소드 호출로 대체한다.

    public class TicketMachine
    {
        private IState currentState;
    
        public void SetState(IState state)
        {
            currentState = state;
        }
    
        private void insertCoin()
        {
            currentState.insertCoin(this);
        }
    
        private void printTicket()
        {
            currentState.printTicket(this);
        }
    
        public void input()
        {
            if (Console.ReadLine() == "coin")
                insertCoin();
            else
                printTicket();
        }
    }
  6. 상태 클래스의 인스턴스를 컨텍스트에 전달하는 것으로 컨텍스트의 상태를 전환할 수 있다.

    • 결과

      coin
      동전을 넣었습니다.
      coin
      이미 동전이 들어있습니다.
      ticket
      티켓을 뽑았습니다.
      ticket
      동전이 없습니다. 동전을 넣어주세요.
      
    • 전체 코드

      class main
      {
          static void Main(string[] args)
          {
              TicketMachine ticketMachine = new TicketMachine();
      
              while(true)
                  ticketMachine.input();
          }
      }
      
      public interface IState
      {
          void insertCoin(TicketMachine ticketMachine);
          void printTicket(TicketMachine ticketMachine);
      }
      
      public class CoinState : IState
      {
          public void insertCoin(TicketMachine ticketMachine)
          {
              Console.WriteLine("이미 동전이 들어있습니다.");
          }
      
          public void printTicket(TicketMachine ticketMachine)
          {
              Console.WriteLine("티켓을 뽑았습니다.");
              ticketMachine.SetState(new NoCoinState());
          }
      }
      
      public class NoCoinState : IState
      {
          public void insertCoin(TicketMachine ticketMachine)
          {
              Console.WriteLine("동전을 넣었습니다.");
              ticketMachine.SetState(new CoinState());
          }
      
          public void printTicket(TicketMachine ticketMachine)
          {
              Console.WriteLine("동전이 없습니다. 동전을 넣어주세요.");
          }
      }
      
      public class TicketMachine
      {
          private IState currentState = new NoCoinState();
      
          public void SetState(IState state)
          {
              currentState = state;
          }
      
          private void insertCoin()
          {
              currentState.insertCoin(this);
          }
      
          private void printTicket()
          {
              currentState.printTicket(this);
          }
      
          public void input()
          {
              if (Console.ReadLine() == "coin")
                  insertCoin();
              else
                  printTicket();
          }
      }

참고

0개의 댓글

관련 채용 정보