Post

상태 패턴 개념 정리(강의 - 얄팍한 코딩사전)

객체지향 디자인 패턴 중 하나인 상태(State) 패턴에 대한 개념 정리 및 예제 코드

상태 패턴 개념 정리(강의 - 얄팍한 코딩사전)

들어가며

소프트웨어 개발에서는 객체의 상태에 따라 동작이 달라져야 하는 경우가 많다. 예를 들어, 문이 열리거나 닫혀 있는 상태에 따라 문을 여는 동작이나 닫는 동작이 달라질 수 있다. 상태(State) 패턴은 이러한 상태 변화에 따른 동작을 효과적으로 관리하기 위해 설계된 객체지향 디자인 패턴이다.

상태 패턴은 객체의 상태를 캡슐화하고, 상태에 따른 행동을 분리하여 유지보수성을 높이고 코드를 더욱 유연하게 만들어준다. 이 패턴을 사용하면 객체가 스스로 상태를 관리하며, 상태 전환에 따른 복잡한 조건문을 피할 수 있다.

이 포스트는 ‘얄팍한 코딩사전’ 유튜브 채널의 05. 상태(State) 패턴 강의를 참고하여 작성되었다. 강의는 Java로 진행되었으나, 이 포스트에서는 예제 코드를 C++로 작성하였다.


선수 지식

State 패턴을 이해하기 위해 필요한 선수 지식은 다음과 같다.

  • 클래스, 객체, 인터페이스, 다향성 등의 객체지향 프로그래밍 개념.
  • 특히, 다형성의 개념을 이해하면 상태 패턴을 설계하는 데 도움이 된다.

상태(State) 패턴

상태(State) 패턴은 객체가 상태에 따라 동작이 달라지는 경우, 각 상태를 별도의 클래스로 정의하고 상태 전환을 관리하는 디자인 패턴이다. 객체는 상태 변화를 통해 자신의 행동을 변화시키며, 상태별로 행동을 구체적으로 구현함으로써 코드의 유연성과 확장성을 높일 수 있다.

즉, 상태 패턴을 사용하면 객체의 상태를 분리하여 상태별 행동을 독립적으로 관리할 수 있다. 상태 전환을 외부에서 제어하는 대신, 상태 자체가 객체의 행동을 변경하는 방식으로 동작을 제어한다.

상태 패턴의 역할

상태 패턴은 객체의 상태에 따라 그 객체의 행동이 달라지는 경우, 상태 전환을 효과적으로 관리하는 방법을 제공한다. 각 상태를 별도의 클래스로 분리하고, 해당 상태에 따른 행동을 정의하는 방식으로 코드의 복잡성을 줄이고 확장성을 높인다.

언제 상태 패턴을 사용해야 하는가?

  • 객체의 동작이 상태에 따라 달라지는 경우: 객체가 다양한 상태를 가질 수 있고, 각 상태에서 동작이 달라져야 할 때 사용하면 유용하다.
  • 상태 전환이 자주 발생하는 경우: 상태 전환에 따른 조건문이 복잡해질 때, 상태 패턴을 사용하여 각 상태에 따른 동작을 개별적으로 관리할 수 있다.
  • 유지보수성과 확장성이 중요한 경우: 새로운 상태나 행동을 추가할 때 기존 코드를 수정하지 않고도 쉽게 확장할 수 있다.

상태 패턴의 구조

상태 패턴은 크게 3가지 주요 구성 요소로 나눌 수 있다.

  1. Context (문맥 클래스): 현재 상태를 관리하며, 상태 전환을 담당하는 클래스이다. 현재 상태에 따라 다른 동작을 실행하고, 상태가 전환되면 새로운 상태로 변경된다.
  2. State (상태 인터페이스): 상태에 따른 행동을 정의하는 인터페이스 또는 추상 클래스이다. 각 상태 클래스는 이 인터페이스를 구현하여 상태별 행동을 정의한다.
  3. ConcreteState (구체 클래스): State 인터페이스를 구현한 구체 클래스이다. 각 상태에 따른 구체적인 행동을 정의하며, 상태 전환이 필요한 경우 Context 클래스에 새로운 상태를 설정할 수 있다.

게임으로 State 이해하기

상태 패턴은 게임 개발에서 캐릭터의 상태에 따라 행동이 달라지는 상황을 효과적으로 처리할 수 있다. 예를 들어, 캐릭터가 일반 상태일 때는 걸을 수 있지만, 점프 상태일 때는 점프 동작을 수행하고, 공격 상태일 때는 적을 공격하는 방식으로 동작이 달라진다.

예시:

  1. Context 클래스 (Character): 캐릭터의 상태를 관리하는 클래스이다. 캐릭터의 상태에 따라 행동이 달라지며, 상태 전환이 발생할 때마다 새로운 상태로 전환된다.
  2. State 인터페이스: 캐릭터의 상태별 행동을 정의하는 인터페이스이다.
  3. ConcreteState 클래스 (WalkingState, JumpingState, AttackingState): 각각의 상태에 따른 구체적인 행동을 정의한다. 예를 들어, WalkingState에서는 걷기 동작을 수행하고, JumpingState에서는 점프 동작을 수행하며, AttackingState에서는 공격 동작을 수행한다.

상태 패턴이 필요한 이

  1. 코드 복잡성 감소: 상태 패턴은 상태별로 코드가 분리되기 때문에, 복잡한 조건문을 사용하지 않고도 객체의 상태에 따른 행동을 정의할 수 있다. 이를 통해 코드의 가독성을 높이고, 유지보수를 쉽게 할 수 있다.
  2. 유연한 상태 전환: 새로운 상태를 추가하거나 상태 전환을 변경해야 할 때, 상태 패턴을 사용하면 기존 코드를 수정하지 않고도 새로운 상태를 쉽게 추가할 수 있다. 이는 OCP(Open-Closed Principle)를 준수하는 설계 방식이다.
  3. 상태별 행동의 분리: 각 상태별로 행동을 독립적으로 관리하기 때문에, 상태 간의 의존성이 줄어들고, 상태별 행동을 확장하거나 수정할 때 다른 상태에 영향을 미치지 않는다.

예제 코드

img1

[main.cpp]


img2

[실행 결과]


한 눈에 코드를 보기 위해 선언부(.h)와 구현부(.cpp)로 분리하지 않고, main.cpp에서 통합하여 코드를 구현했다.

실제로 C++로 코딩할 때는 선언부와 구현부를 나눠서 코딩하는 것을 추천한다. 프로젝트가 커질 수록 선언부와 구현부로 나눠서 작업해야 유지 보수하기 편리하고, 효율적이기 때문이다.

코드 설명

1.CharacterState 클래스(상태 인터페이스)

img3

[ScoreManager 클래스(싱글톤 클래스)]


  • 상태 인터페이스: CharacterState는 추상 클래스이고, 캐릭터의 행동을 정의하는 인터페이스 역할을 한다. 여기서는 상태에 따라 캐릭터의 다른 행동이 호출될 수 있도록 HandleInput()이라는 순수 가상 함수를 가지고 있다. 이를 통해 각 상태 클래스에서 이 메소드를 구현하여 캐릭터의 상태에 따른 동작을 정의할 수 있다.
  • 가상 소멸자: ~CharacterState() 가상 소멸자는 상속받은 클래스가 소멸될 때 올바르게 메모리를 해제하도록 설계되었다.

2.WalkingState, JumpingState, AttackingState 클래스(구체 클래스)

img4

[WalkingState, JumpingState, AttackingState 클래스(구체 클래스)]


  • WalkingState: 캐릭터가 걷는 상태를 정의하는 클래스이다. HandleInput() 메소드를 통해 “캐릭터가 걷고 있습니다.”라는 메시지를 출력한다.
  • JumpingState: 캐릭터가 점프하는 상태를 정의하는 클래스이다. HandleInput() 메소드를 통해 “캐릭터가 점프하고 있습니다.”라는 메시지를 출력한다.
  • AttackingState: 캐릭터가 공격하는 상태를 정의하는 클래스이다. HandleInput() 메소드를 통해 “캐릭터가 공격하고 있습니다.”라는 메시지를 출력한다.

구체 클래스들은 모두 CharacterState 인터페이스를 구현해서 캐릭터가 상태에 따라 행동을 다르게 할 수 있도록 했다.

3. Character 클래스(Context 클래스)

img5

[Character 클래스(Context 클래스)]


  • Character 클래스: 캐릭터의 현재 상태를 관리하는 Context 클래스이다. 캐릭터의 상태는 CharacterState 인터페이스를 구현한 객체로 설정되고, 이를 통해 캐릭터의 동작이 상태에 따라 변경할 수 있다.
  • SetState(): 캐릭터의 상태를 변경하는 메소드로, CharacterState 객체를 인자로 받아 현재 상태를 변경한다.
  • HandleInput(): 캐릭터의 현재 상태에 따른 동작을 처리하는 메소드이다. 캐릭터의 상태가 설정되어 있을 경우, 해당 상태의 HandleInput() 메소드를 호출하여 행동을 수행한다. 상태가 설정되지 않았다면 “상태가 설정되지 않았습니다.”라는 메시지를 출력한다.

4. 클라이언트(main함수)

img6

[클라이언트(main함수)]


  • 상태 전환: SetState() 메소드를 사용하여 캐릭터의 상태를 WalkingState, JumpingState, AttackingState로 변경하고, 각각의 상태에서 HandleInput()을 호출하여 상태에 따른 동작을 출력한다.
  • 동작 처리: 걷기, 점프, 공격 상태로 상태를 변경한 후 HandleInput() 메소드가 호출되면, 상태에 맞는 메시지를 출력하면서 각각의 동작을 수행한다.

Q&A

Q1. 상태(State) 패턴을 사용하면 어떤 점이 좋은가?

  • A1: 상태 패턴을 사용하면 객체의 상태 전환에 따른 복잡한 조건문을 줄일 수 있다. 또한, 상태별 동작을 독립적으로 관리할 수 있다. 이를 통해 코드의 가독성과 유지보수성이 높일 수 있고, 새로운 상태를 쉽게 추가할 수 있어 OCP(개방-폐쇄 원칙)를 준수할 수 있다.

    *OCP(개발-폐쇄 원칙): 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있는 원칙.

Q2. 상태(State) 패턴과 전략(Strategy) 패턴의 차이점은 무엇인가?

  • A2: 상태 패턴과 전략 패턴은 유사하게 보일 수 있지만, 상태 패턴은 객체의 내부 상태에 따라 행동이 변경되는 경우에 사용되며, 상태 전환이 객체의 관리 하에 자동으로 이루어진다. 전략 패턴은 특정 알고리즘을 런타임에 선택하고 교체할 수 있는 구조로, 전략을 클라이언트가 직접 설정하고 변경한다고 한다.

Q3. 상태(State) 패턴을 언제 사용해야 하는가?

  • A3: 객체가 여러 가지 상태를 가지며, 각 상태에 따라 동작이 달라지는 경우 상태 패턴을 사용하면 유용하다. 특히, 상태 전환이 자주 발생하고, 상태에 따라 객체의 행동이 크게 달라질 때 상태 패턴을 사용하면 코드의 복잡성을 줄이고, 상태 추가나 변경이 쉬워진다.

결론

상태(State) 패턴은 객체의 상태에 따라 동작이 달라지는 복잡한 시스템에서 매우 유용한 디자인 패턴이다. 이 패턴을 통해 각 상태를 독립적으로 관리할 수 있으며, 상태 변화에 따른 조건문을 줄여 코드의 복잡성을 줄일 수 있다.

특히 게임과 같은 시스템에서 캐릭터의 행동이 상태에 따라 달라지는 경우, 상태 패턴을 적용하면 캐릭터의 행동을 유연하게 변경하고 확장할 수 있다. 새로운 상태를 추가하거나 수정할 때, 기존 코드를 수정할 필요 없이 간단하게 확장할 수 있어 유지보수성이 뛰어나다고 한다.

또한 상태 패턴은 객체가 스스로 상태 전환을 처리할 수 있도록 만들어, 상태 전환 로직을 Context 클래스에 집중시키지 않고 각 상태 클래스에서 처리하게 함으로써 코드의 가독성과 유연성을 높일 수 있다.

결론적으로 상태 패턴은 복잡한 상태 전환을 관리하고, 상태에 따른 행동을 유연하게 처리할 수 있는 효율적인 디자인 패턴이다. 게임 개발과 같은 동적인 시스템에서 상태 패턴을 적용하면 코드의 재사용성확장성을 높이고, 유지보수를 용이하게 할 수 있다.

중첩 if문으로 시스템 로직 개발을 지양하고, 상태 패턴이 적용된 시스템 로직 개발을 지향할 수 있도록 더 배우면서 성장할 필요성을 느꼈다.

함께 보면 좋은 자료

함께 보면 좋은 자료는 다음과 같다.

참고 자료

본 포스트를 작성할 때 참고한 자료들이다.

  • 유튜브 채널 얄팍한 코딩사전: 05. 상태(State) 패턴 - 본 포스트는 이 강의를 참고하여 작성되었다.
This post is licensed under CC BY 4.0 by the author.