본문 바로가기
디자인패턴

상태 패턴의 장단점과 최적의 사용 사례(State Pattern)

by 대박플머 2024. 10. 17.

- 상태 패턴(State Pattern) 소개 
- 상태 패턴의 구체적인 구현(State Pattern)
- 상태 패턴의 응용 사례(State Pattern)
- 상태 패턴의 확장과 변형(State Pattern)
- 상태 패턴의 장단점과 최적의 사용 사례(State Pattern)

 

이번 글에서는 상태 패턴(State Pattern)의 장단점을 종합적으로 분석하고, 상태 패턴을 실무에서 최적으로 사용하는 방법을 다뤄보겠습니다. 상태 패턴은 객체의 상태에 따라 동작을 변경할 수 있도록 하는 강력한 디자인 패턴입니다. 그러나 모든 상황에서 사용하기 적합하지 않으며, 장점과 단점을 고려하여 상황에 맞게 적용하는 것이 중요합니다. 상태 패턴의 장단점, 성능 문제, 테스트 전략, 그리고 최적의 사용 사례를 구체적으로 설명하겠습니다.


1. 상태 패턴의 주요 장점

상태 패턴을 사용하면 여러 가지 이점을 얻을 수 있습니다. 특히, 상태에 따른 행동이 복잡하거나 상태 전환이 자주 일어나는 시스템에서 매우 유용합니다.

1.1 복잡한 조건문 제거

상태 패턴의 가장 큰 장점 중 하나는 복잡한 조건문을 제거할 수 있다는 점입니다. 상태별로 행동이 달라지는 경우, 전통적인 방식으로는 상태를 구분하기 위해 다중 조건문(예: if-else, switch-case)을 사용하는 경우가 많습니다. 그러나 조건문이 많아질수록 코드가 복잡해지고 유지보수하기 어려워집니다.

상태 패턴을 사용하면 이러한 조건문을 없애고, 각 상태를 객체로 분리하여 상태별 행동을 정의할 수 있습니다. 이를 통해 가독성과 코드의 유지보수성이 크게 향상됩니다.

예를 들어, 전통적인 방식의 조건문을 사용한 코드가 아래와 같다면:

if (currentState === "idle") {
  // idle 상태의 동작
} else if (currentState === "running") {
  // running 상태의 동작
} else if (currentState === "paused") {
  // paused 상태의 동작
}

상태 패턴을 적용한 코드는 다음과 같이 간결해집니다:

currentState.handle();

상태마다 별도의 클래스가 정의되어 있으므로, 조건문 없이 상태 전환과 동작 처리가 이루어집니다.

1.2 유지보수성 향상

상태 패턴은 각 상태별로 행동을 분리함으로써 유지보수성이 크게 향상됩니다. 새로운 상태가 추가되거나 기존 상태의 행동이 변경될 때, 관련된 상태 클래스만 수정하거나 추가하면 됩니다. 이를 통해 전체 코드를 변경할 필요가 없으므로, 상태가 늘어나도 코드의 복잡도가 급격히 증가하지 않습니다.

예를 들어, 새로운 상태가 추가되거나 동작이 바뀌어도 관련된 클래스만 수정하면 되므로 코드 수정 범위가 최소화됩니다.

1.3 캡슐화된 상태 관리

상태 패턴을 사용하면 상태 전환 로직이 상태 객체 내부에 캡슐화됩니다. 이는 상태 변화가 발생할 때 상태 전환에 대한 책임이 각 상태 객체에 있다는 것을 의미합니다. 이를 통해 상태 전환 로직이 외부에 드러나지 않으며, 상태 전환과 관련된 버그가 줄어듭니다.

1.4 확장성

상태 패턴은 확장성이 뛰어납니다. 새로운 상태를 쉽게 추가할 수 있으며, 기존 상태와 동작에 영향을 미치지 않고 상태 전환 로직을 유연하게 확장할 수 있습니다. 상태가 많아질수록 이 장점은 더욱 부각됩니다.


2. 상태 패턴의 주요 단점

상태 패턴은 여러 가지 이점을 제공하지만, 모든 경우에 적합한 것은 아닙니다. 상태 패턴을 적용할 때 몇 가지 단점도 고려해야 합니다.

2.1 객체 수 증가

상태 패턴을 사용하면 상태별로 클래스를 정의해야 하기 때문에 객체 수가 증가할 수 있습니다. 상태가 많아질수록 객체의 수가 늘어나며, 코드의 복잡성이 증가할 수 있습니다. 특히, 시스템에서 관리해야 하는 상태가 매우 많을 경우 객체 지향 설계가 오히려 불편하게 느껴질 수 있습니다.

2.2 상태 전환 관리의 복잡성

상태가 복잡해지면 상태 전환 로직도 복잡해질 수 있습니다. 상태 전환이 많은 시스템에서는 상태 패턴을 사용하는 것이 오히려 상태 전환 로직을 복잡하게 만들 수 있으며, 상태 전환에 관련된 버그가 발생할 가능성이 높아집니다.

예를 들어, 상태가 여러 개로 분화되어 있고, 각 상태에서 특정 조건에 따라 다양한 상태로 전환되어야 하는 경우에는 상태 전환을 관리하기 위한 복잡한 로직이 필요할 수 있습니다.

2.3 성능 문제

상태 패턴을 사용할 때 객체 생성과 상태 전환이 자주 일어나면 성능 문제가 발생할 수 있습니다. 특히, 상태 객체를 계속해서 생성하고 소멸시키는 과정에서 메모리와 CPU 리소스가 낭비될 수 있습니다. 상태 전환이 빈번한 시스템에서는 이러한 성능 저하가 눈에 띌 수 있습니다.

이러한 문제를 해결하기 위해 상태 객체를 미리 생성해두고 상태 전환 시 재사용하는 방식(싱글톤 패턴이나 Flyweight 패턴 결합)을 사용할 수 있습니다.

2.4 테스트 복잡성 증가

상태 패턴은 각 상태가 독립적이므로 테스트가 복잡해질 수 있습니다. 상태 전환 로직을 제대로 테스트하려면 각 상태에 대한 테스트와 상태 간 전환을 고려한 테스트 케이스를 모두 작성해야 합니다. 상태가 많아질수록 테스트해야 할 시나리오도 증가하므로, 테스트 케이스 작성에 더 많은 리소스가 필요할 수 있습니다.


3. 성능 문제와 해결 방법

상태 패턴을 사용할 때 성능 문제가 발생할 수 있는 주요 원인은 상태 객체의 생성 및 소멸입니다. 이러한 성능 문제를 해결하기 위한 방법으로는 상태 객체를 재사용하거나 싱글톤 패턴을 사용하는 방법이 있습니다.

3.1 상태 객체의 재사용

상태 객체를 매번 생성하는 대신, 미리 생성해두고 상태 전환 시 재사용하는 방식으로 성능을 최적화할 수 있습니다. 예를 들어, 상태 객체를 한 번만 생성하고, 상태 전환 시 해당 객체를 다시 사용하는 방법을 사용할 수 있습니다.

class ATMMachine {
  private idleState: IdleState;
  private runningState: RunningState;

  constructor() {
    this.idleState = new IdleState(this);
    this.runningState = new RunningState(this);
  }

  getIdleState(): IdleState {
    return this.idleState;
  }

  getRunningState(): RunningState {
    return this.runningState;
  }

  // 상태 전환 시 재사용
}

위 예제에서는 IdleStateRunningState 객체를 미리 생성해두고, 상태 전환 시 동일한 객체를 재사용하여 성능을 최적화할 수 있습니다.

3.2 싱글톤 패턴과의 결합

싱글톤 패턴을 상태 패턴과 결합하여 하나의 상태 객체만 생성하도록 구현할 수 있습니다. 이를 통해 상태 객체가 여러 번 생성되는 것을 방지하고 메모리 사용량을 줄일 수 있습니다.

class IdleState {
  private static instance: IdleState;

  private constructor() {}

  static getInstance(): IdleState {
    if (!IdleState.instance) {
      IdleState.instance = new IdleState();
    }
    return IdleState.instance;
  }
}

const idleState = IdleState.getInstance();

위 예제에서는 싱글톤 패턴을 사용하여 IdleState 객체를 하나만 생성하고, 이를 여러 곳에서 재사용하도록 했습니다. 이를 통해 상태 객체가 매번 생성되는 것을 방지할 수 있습니다.


4. 상태 패턴의 테스트 전략

상태 패턴을 테스트할 때는 각 상태에 대한 독립적인 테스트와 상태 전환에 대한 테스트를 모두 고려해야 합니다.

4.1 상태별 테스트

각 상태는 독립적으로 동작해야 하므로, 상태별로 테스트를 작성해야 합니다. 예를 들어, 대기 상태(IdleState), 달리기 상태(RunningState), 공격 상태(AttackingState)에 대한 테스트를 작성할 수 있습니다.

describe("IdleState", () => {
  it("should transition to running state when 'run' is input", () => {
    const character = new GameCharacter();
    character.setState(new IdleState());

    character.handleInput("run");

    expect(character.getState()).toBeInstanceOf(RunningState);
  });
});

4.2 상태 전환 테스트

상태 패턴의 핵심은 상태 전환이므로, 상태 전환 로직을 테스트하는 것도 중요합니다. 상태 전환 시 올바른 상태로 전환되는지, 각 상태에서 특정 입력에 따라 적절히 전환되는지 확인해야 합니다.

describe("State Transition", () => {
  it("should transition from idle to running to attacking", () => {
    const character = new GameCharacter();
    character.handleInput("run");
    expect(character.getState()).toBeInstanceOf(RunningState);

    character.handleInput("attack");
    expect(character.getState()).toBeInstanceOf(AttackingState);
  });
});

이와 같은 테스트 케이스를 작성하여 상태별 행동과 상태 전환 로직을 철저히 검증할 수 있습니다.


5. 상태 패턴의 최적의 사용 사례

상태 패턴을 적용하기에 적합한 시나리오는 다음과 같습니다.

5.1 상태가 자주 변경되는 시스템

상태가 자주 변경되거나 상태별로 다른 동작을 수행해야 하는 시스템에서는 상태 패턴이 매우 유용합니다. 예를 들어, 게임 캐릭터의 상태 관리(대기, 달리기, 공격 등)나 UI 상태 관리(로그인, 로그아웃, 대기 상태)와 같은 시나리오에 적합합니다.

5.2 상태별 동작이 복잡한 시스템

상태별로 복잡한 동작을 수행해야 하는 경우 상태 패턴을 사용하면 동작을 각 상태 객체로 분리하여 관리할 수 있습니다. 이를 통해 코드를 더 쉽게 유지보수할 수 있으며, 상태별로 동작을 추가하거나 변경하기가 용이해집니다.

5.3 확장 가능성이 높은 시스템

상태가 점차 증가하거나 상태 전환 로직이 복잡해질 것으로 예상되는 시스템에서는 상태 패턴이 매우 효과적입니다. 새로운 상태가 추가되더라도 기존 코드를 수정할 필요 없이 새로운 상태 클래스만 추가하면 되므로, 확장성이 뛰어납니다.


6. 결론

이번 글에서는 상태 패턴의 장단점과 최적의 사용 사례를 살펴보았습니다. 상태 패턴은 복잡한 상태 전환 로직을 간결하게 관리하고, 상태별 동작을 객체로 분리하여 유지보수성과 확장성을 높일 수 있는 강력한 패턴입니다. 그러나 상태가 많아질수록 객체 수가 증가하거나 상태 전환 관리가 복잡해질 수 있으므로, 이러한 단점을 해결하기 위한 성능 최적화 방법과 테스트 전략을 함께 고려해야 합니다.

상태 패턴은 상태 전환이 자주 일어나는 시스템에서 매우 유용하며, 이를 적절히 사용하면 코드를 더 깨끗하고 유지보수하기 쉽게 만들 수 있습니다.