본문 바로가기
Study/디자인 패턴

[C#/Unity][디자인패턴] 상태 패턴(State Pattern)

by 스테디코디스트 2023. 12. 27.
반응형

1. 상태 패턴이란?

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

- 상태를 조건문으로 검사해서 행위를 달리하는 것이 아닌, 상태를 객체화하여 상태가 행동을 할 수 있도록 위임하는 패턴.

- 객체의 특정 상태를 클래스로 선언하고, 클래스에서는 해당 상태에서 할 수 있는 행위들을 메서드로 정의한다.

- 상태란, 객체가 가질 수 있는 어떤 조건이나 상황을 의미한다.

 

2. 상태 패턴을 사용하는 경우

- 상태 전이를 위한 로직이 지나치게 복잡한 경우

- 현재 상태에 따라 다르게 행동하는 객체가 있는 경우

- 상태들의 수가 많고, 상태별로 코드가 자주 변경되는 경우

 

3. 상태 패턴의 장점

- 객체 내부 상태 변경에 따라 객체의 행동을 상태에 특화된 행동들로 분리해 낼 수 있고, 새로운 행동을 추가하더라도 다른 행동에 영향을 주지 않는다.

- 상태(State)에 따른 동작을 개별 클래스로 옮겨서 관리 할 수 있다.

- 상태(State)와 관련된 모든 동작을 각각의 상태 클래스에 분산시킴으로써, 코드 복잡도를 줄일 수 있다.

- 특정 상태와 관련된 코드를 별도의 클래스로 구성하여 단일 책임 원칙을 준수할 수 있다.

- 기존 상태(State) 클래스나 컨텍스트를 변경하지 않고 새로운 상태를 도입할 수 있어, 개방 폐쇄 원칙을 준수할 수 있다.

- 하나의 상태 객체만 사용하여 상태 변경을 하므로 일관성 없는 상태 주입을 방지하는데 도움이 된다.

 

4. 상태 패턴의 단점

- 상태 별로 클래스를 생성하므로, 관리해야할 클래스의 수가 증가한다.

- 상태 클래스의 갯수가 많고, 규칙이 자주 변경된다면, Context의 상태 변경 코드가 복잡해질 수 있다.

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

 

5. 코드 구현

1) 구조

- State 인터페이스 

: 상태를 추상화한 고수준 모듈

- Concrete State 

: 구체적인 각각의 상태를 클래스로 표현.

: State 역할로 결정되는 인터페이스(API)를 구체적으로 구현.

: 다음 상태가 결정되면 Context에 상태 변경을 요청하는 역할 수행.

- Context

: State를 이용하는 시스템.

: 시스템 상태를 나타내는 State 객체를 합성(Composition)하여 가지고 있음.

: 클라이언트로부터 요청받으면 State 객체에 행위 실행을 위임함.

 

2) 소스코드(C#)

- 패턴 적용 전

using System;
using System.Collections.Generic;
using System.Threading;

namespace StudyCSharp
{
    class Player
    {
        // 상태를 나타내는 enum 타입
        public enum STATE
        {
            Idle,       // 기본 상태
            Attack,     // 공격 상태
            Defense,    // 방어 상태
            Die,        // 죽은 상태
        }

        // 상태를 저장하는 변수
        public STATE myState;

        public Player()
        {
            // 초기상태 저장
            Console.WriteLine(">>>플레이어 생성<<<");
            this.myState = STATE.Idle;
        }

        // 상태 변경
        void ChangeState(STATE state)
        {
            this.myState = state;
        }

        // 데미지를 입음
        public void OnDamage()
        {
            if (myState == STATE.Idle)
            {
                Console.WriteLine(">>>플레이어 죽음<<<");
                ChangeState(STATE.Die);
            }
            else if (myState == STATE.Attack)
            {
                Console.WriteLine("상대와 같이 죽음");
                ChangeState(STATE.Die);
            }
            else if (myState == STATE.Defense)
            {
                Console.WriteLine("막음, 방어 모드 Off");
                ChangeState(STATE.Idle);
            }
            else if (myState == STATE.Die)
            {
                Console.WriteLine("이미 죽은 상태");
            }
        }

        // 공격 키 입력
        public void OnAttack()
        {
            if (myState == STATE.Idle)
            {
                Console.WriteLine("공격 개시");
                ChangeState(STATE.Attack);
            }
            else if (myState == STATE.Attack)
            {
                Console.WriteLine("이미 공격 중");
            }
            else if (myState == STATE.Defense)
            {
                Console.WriteLine("공격 상태로 변경");
                ChangeState(STATE.Attack);
            }
            else if (myState == STATE.Die)
            {
                Console.WriteLine("죽은 상태이므로 공격 불가");
            }
        }

        public void OnDefense()
        {
            Console.WriteLine("방어 모드 On");
            myState = STATE.Defense;
        }

        // 현재 상태 출력
        public void curStatePrint()
        {
            if (myState == STATE.Idle)
            {
                Console.WriteLine("기본(Idle) 상태");
            }
            else if (myState == STATE.Attack)
            {
                Console.WriteLine("공격(Attack) 상태");
            }
            else if (myState == STATE.Defense)
            {
                Console.WriteLine("방어(Defense) 상태");
            }
            else if (myState == STATE.Die)
            {
                Console.WriteLine("죽은(Die) 상태");
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("-------------------------");

            Player player = new Player();
            player.curStatePrint();

            Console.WriteLine("-------------------------");

            // 공격 진행 : Idle -> Attack
            player.OnAttack();
            player.curStatePrint();

            Console.WriteLine("-------------------------");

            // 방어 상태로 변경 : Attack -> Defense
            player.OnDefense();
            player.curStatePrint();

            Console.WriteLine("-------------------------");

            // 데미지 입음 -> 막음 -> 방어모드 해제 : Defense -> Idle
            player.OnDamage();
            player.curStatePrint();

            Console.WriteLine("-------------------------");

            // 한 번 더 데미지 입음 -> 죽음 : Idle -> Die
            player.OnDamage();
            player.curStatePrint();

            Console.WriteLine("-------------------------");
        }
    }
}

 

- 실행 결과

 

- 패턴 적용 후

using System;

namespace StudyCSharp
{
    interface PlayerState
    {
        void OnAttack(PlayerContext pc);

        void OnDamage(PlayerContext pc);

        string print();
    }

    class IdleState : PlayerState
    {
        #region 싱글톤
        private static class SingleInstanceHolder
        {
            private static IdleState _instance;
            public static IdleState Instance
            {
                get
                {
                    if (_instance == null)
                        _instance = new IdleState();

                    return _instance;                        
                }
            }
        }

        public static IdleState getInstance()
        {
            return SingleInstanceHolder.Instance;
        }
        #endregion        

        public void OnAttack(PlayerContext pc)
        {
            Console.WriteLine("공격 개시");
            //pc.ChangeState(new AttackState()); -> 이렇게 하면 매번 새 인스턴스를 생성시켜 메모리를 낭비하게 될 수 있음 -> 싱글톤 패턴 사용
            pc.ChangeState(AttackState.getInstance());
        }

        public void OnDamage(PlayerContext pc)
        {
            Console.WriteLine(">>>플레이어 죽음<<<");
            pc.ChangeState(DieState.getInstance());
        }

        public string print()
        {
            return "기본(Idle) 상태";
        }
    }

    class AttackState : PlayerState
    {
        #region 싱글톤
        private static class SingleInstanceHolder
        {
            private static AttackState _instance;
            public static AttackState Instance
            {
                get
                {
                    if (_instance == null)
                        _instance = new AttackState();

                    return _instance;
                }
            }
        }

        public static AttackState getInstance()
        {
            return SingleInstanceHolder.Instance;
        }
        #endregion

        public void OnAttack(PlayerContext pc)
        {
            Console.WriteLine("이미 공격 중");
        }

        public void OnDamage(PlayerContext pc)
        {
            Console.WriteLine("상대와 같이 죽음");
            pc.ChangeState(DieState.getInstance());
        }

        public string print()
        {
            return "공격(Attack) 상태";
        }
    }

    class DefenseState : PlayerState
    {
        #region 싱글톤
        private static class SingleInstanceHolder
        {
            private static DefenseState _instance;
            public static DefenseState Instance
            {
                get
                {
                    if (_instance == null)
                        _instance = new DefenseState();

                    return _instance;
                }
            }
        }

        public static DefenseState getInstance()
        {
            return SingleInstanceHolder.Instance;
        }
        #endregion

        public void OnAttack(PlayerContext pc)
        {
            Console.WriteLine("공격 상태로 변경");
            pc.ChangeState(AttackState.getInstance());
        }

        public void OnDamage(PlayerContext pc)
        {
            Console.WriteLine("막음, 방어모드 Off");
            pc.ChangeState(IdleState.getInstance());
        }

        public string print()
        {
            return "방어(Defense) 상태";
        }
    }

    class DieState : PlayerState
    {
        #region 싱글톤
        private static class SingleInstanceHolder
        {
            private static DieState _instance;
            public static DieState Instance
            {
                get
                {
                    if (_instance == null)
                        _instance = new DieState();

                    return _instance;
                }
            }            
        }

        public static DieState getInstance()
        {
            return SingleInstanceHolder.Instance;
        }
        #endregion

        public void OnAttack(PlayerContext pc)
        {
            Console.WriteLine("죽은 상태이므로 공격 불가");
        }

        public void OnDamage(PlayerContext pc)
        {
            Console.WriteLine("이미 죽은 상태");
        }

        public string print()
        {
            return "죽은(Die) 상태";
        }
        
    }

    class PlayerContext
    {
        PlayerState myState;

        public PlayerContext()
        {
            Console.WriteLine(">>>플레이어 생성<<<");
            myState = new IdleState();
        }

        public void ChangeState(PlayerState state)
        {
            myState = state;
        }

        public void OnDefense()
        {
            Console.WriteLine("방어 모드");
            ChangeState(new DefenseState());
        }

        public void OnAttack()
        {
            myState.OnAttack(this);
        }

        public void OnDamage()
        {
            myState.OnDamage(this);
        }

        public void curStatePrint()
        {
            Console.WriteLine(myState.print());
        }
    }    

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("-------------------------");

            PlayerContext player = new PlayerContext();
            player.curStatePrint();

            Console.WriteLine("-------------------------");

            // 공격 진행 : Idle -> Attack
            player.OnAttack();
            player.curStatePrint();

            Console.WriteLine("-------------------------");

            // 방어 상태로 변경 : Attack -> Defense
            player.OnDefense();
            player.curStatePrint();

            Console.WriteLine("-------------------------");

            // 데미지 입음 -> 막음 -> 방어모드 해제 : Defense -> Idle
            player.OnDamage();
            player.curStatePrint();

            Console.WriteLine("-------------------------");

            // 한 번 더 데미지 입음 -> 죽음 : Idle -> Die
            player.OnDamage();
            player.curStatePrint();

            Console.WriteLine("-------------------------");
        }
    }
}

 

- 실행 결과

 


<참고 사이트>

 

[디자인 패턴] 스테이트(상태) 패턴 (State Pattern)

스테이트 패턴 정의, 사용 이유, 구현

velog.io

 

 

💠 상태(State) 패턴 - 완벽 마스터하기

State Pattern 상태 패턴(State Pattern)은 객체가 특정 상태에 따라 행위를 달리하는 상황에서, 상태를 조건문으로 검사해서 행위를 달리하는 것이 아닌, 상태를 객체화 하여 상태가 행동을 할 수 있도

inpa.tistory.com

 

[디자인패턴] 스테이트 패턴 ( State Pattern )

스테이트 패턴 ( State Pattern )스테이트 패턴은 객체가 특정 상태에 따라 행위를 달리하는 상황에서,자신이 직접 상태를 체크하여 상태에 따라 행위를 호출하지 않고,상태를 객체화 하여 상태가

victorydntmd.tistory.com

 

 

스테이트 패턴 (State Pattern)

객체의 내부 상태에 따라 행동을 변경할 수 있다.

johngrib.github.io

 

C#으로 작성된 상태 / 디자인 패턴들

 

refactoring.guru