본문 바로가기
디자인패턴

상태 패턴의 확장과 변형(State Pattern)

by 대박플머 2024. 10. 17.

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

 

이번 글에서는 상태 패턴(State Pattern)의 변형과 확장 가능성에 대해 다루겠습니다. 상태 패턴은 기본적으로 객체의 상태에 따라 다른 행동을 수행하는 디자인 패턴이지만, 이를 변형하거나 다른 패턴과 결합하여 다양한 시나리오에 더 유연하게 적용할 수 있습니다. 상태 패턴은 함수형 프로그래밍과 같은 새로운 패러다임에서도 변형되어 사용될 수 있으며, 전략 패턴과 결합해 더욱 강력한 구조를 제공할 수 있습니다. 이번 글에서는 이러한 다양한 확장 방법과 변형 사례를 자세히 살펴보겠습니다.


1. 상태 패턴의 기본 개념 복습

상태 패턴은 객체의 상태에 따라 행동을 변경하는 디자인 패턴입니다. 이를 통해 복잡한 조건문을 제거하고, 상태에 따라 독립적으로 행동을 정의할 수 있습니다. 상태 패턴은 다음과 같은 요소로 구성됩니다:

  • Context(문맥): 상태 객체를 포함하고, 상태에 따른 동작을 요청받아 처리합니다.
  • State(상태 인터페이스): 상태별로 구현해야 할 공통 행동을 정의하는 인터페이스입니다.
  • ConcreteState(구체적인 상태 클래스): 상태 인터페이스를 구현하여 각 상태에 맞는 행동을 정의하는 클래스입니다.

이 기본 구조를 바탕으로 상태 패턴을 어떻게 확장하고 변형할 수 있는지 살펴보겠습니다.


2. 함수형 프로그래밍에서의 상태 패턴

상태 패턴은 객체 지향 프로그래밍에서 주로 사용되지만, 함수형 프로그래밍에서도 활용할 수 있습니다. 함수형 프로그래밍에서는 상태 객체를 함수로 정의하고, 상태 전환을 함수 호출을 통해 수행하는 방식으로 변형할 수 있습니다.

2.1 함수형 상태 패턴 예제

함수형 프로그래밍에서 상태 패턴을 구현하는 예제로 간단한 상태 전환 로직을 살펴보겠습니다. 이 예제에서는 함수가 상태를 담당하고, 상태에 따라 다른 동작을 수행하게 됩니다.

type State = (input: string) => State;

const idleState: State = (input: string) => {
  if (input === "run") {
    console.log("달리기 상태로 전환됩니다.");
    return runningState;
  } else if (input === "attack") {
    console.log("공격 상태로 전환됩니다.");
    return attackingState;
  } else {
    console.log("대기 상태입니다.");
    return idleState;
  }
};

const runningState: State = (input: string) => {
  if (input === "stop") {
    console.log("대기 상태로 전환됩니다.");
    return idleState;
  } else if (input === "attack") {
    console.log("공격 상태로 전환됩니다.");
    return attackingState;
  } else {
    console.log("달리는 중입니다.");
    return runningState;
  }
};

const attackingState: State = (input: string) => {
  if (input === "stop") {
    console.log("대기 상태로 전환됩니다.");
    return idleState;
  } else if (input === "run") {
    console.log("달리기 상태로 전환됩니다.");
    return runningState;
  } else {
    console.log("공격 중입니다.");
    return attackingState;
  }
};

// 상태 전환 시뮬레이션
let currentState: State = idleState;

currentState = currentState("run");
currentState = currentState("attack");
currentState = currentState("stop");

결과:

달리기 상태로 전환됩니다.
공격 상태로 전환됩니다.
대기 상태로 전환됩니다.

위 예제에서는 상태를 함수로 정의하고, 상태 전환을 함수 호출을 통해 수행합니다. 각 상태는 다음 상태로 전환할 때 새로운 상태를 반환하므로, 상태 전환이 명시적으로 이루어집니다. 이 방식은 함수형 프로그래밍의 장점을 살리면서도 상태 패턴의 개념을 유지할 수 있습니다.

2.2 함수형 상태 패턴의 장점

함수형 프로그래밍에서 상태 패턴을 사용하면 다음과 같은 장점이 있습니다:

  • 상태 관리의 간결함: 상태를 함수로 정의함으로써 상태 객체를 만들 필요 없이 상태 전환 로직을 간결하게 관리할 수 있습니다.
  • 불변성 유지: 함수형 프로그래밍의 특성상 상태는 불변(immutable)하므로, 상태 변화가 발생할 때 기존 상태를 변경하지 않고 새로운 상태를 반환하는 구조를 유지할 수 있습니다.
  • 테스트 용이성: 상태 전환 로직이 순수 함수로 이루어져 있기 때문에, 각 상태 함수는 테스트하기 용이하며, 외부 의존성 없이 독립적으로 테스트할 수 있습니다.

3. 상태 패턴과 전략 패턴의 결합

상태 패턴과 전략 패턴은 구조적으로 유사한 면이 많습니다. 두 패턴 모두 다양한 상태나 전략에 따라 행동을 변경할 수 있도록 설계되었지만, 그 목적과 사용 방식에 차이가 있습니다.

  • 상태 패턴: 객체의 상태에 따라 행동을 변경하는 패턴입니다. 상태 전환이 객체 내부에서 발생하며, 상태가 변경됨에 따라 동작이 달라집니다.
  • 전략 패턴: 객체의 행위를 전략 객체로 캡슐화하여 다양한 알고리즘을 선택적으로 사용할 수 있게 하는 패턴입니다. 클라이언트 코드에서 전략을 선택해 사용할 수 있습니다.

두 패턴을 결합하면 상태 전환과 전략 선택을 유연하게 결합할 수 있는 구조를 만들 수 있습니다. 예를 들어, 상태 패턴으로 상태 전환을 관리하면서도, 각 상태에서 실행할 전략을 선택할 수 있도록 할 수 있습니다.

3.1 상태 패턴과 전략 패턴 결합 예제

아래 예제는 상태 패턴과 전략 패턴을 결합한 코드입니다. 상태에 따라 서로 다른 전략을 사용할 수 있도록 설계되었습니다.

interface AttackStrategy {
  execute(): void;
}

class MeleeAttack implements AttackStrategy {
  execute(): void {
    console.log("근접 공격을 실행합니다.");
  }
}

class RangedAttack implements AttackStrategy {
  execute(): void {
    console.log("원거리 공격을 실행합니다.");
  }
}

interface CharacterState {
  handleInput(character: GameCharacter, input: string): void;
}

class IdleState implements CharacterState {
  handleInput(character: GameCharacter, input: string): void {
    if (input === "run") {
      console.log("달리기 상태로 전환됩니다.");
      character.setState(new RunningState());
    } else if (input === "attack") {
      console.log("공격 상태로 전환됩니다.");
      character.setState(new AttackingState(new MeleeAttack()));
    }
  }
}

class RunningState implements CharacterState {
  handleInput(character: GameCharacter, input: string): void {
    if (input === "stop") {
      console.log("대기 상태로 전환됩니다.");
      character.setState(new IdleState());
    } else if (input === "attack") {
      console.log("공격 상태로 전환됩니다.");
      character.setState(new AttackingState(new RangedAttack()));
    }
  }
}

class AttackingState implements CharacterState {
  private strategy: AttackStrategy;

  constructor(strategy: AttackStrategy) {
    this.strategy = strategy;
  }

  handleInput(character: GameCharacter, input: string): void {
    this.strategy.execute();
    if (input === "stop") {
      console.log("대기 상태로 전환됩니다.");
      character.setState(new IdleState());
    }
  }
}

class GameCharacter {
  private state: CharacterState;

  constructor() {
    this.state = new IdleState(); // 초기 상태는 Idle(대기) 상태
  }

  setState(state: CharacterState): void {
    this.state = state;
  }

  handleInput(input: string): void {
    this.state.handleInput(this, input);
  }
}

// 상태 및 전략 전환 시뮬레이션
const character = new GameCharacter();

character.handleInput("run"); // 달리기 상태로 전환됩니다.
character.handleInput("attack"); // 원거리 공격을 실행합니다.
character.handleInput("stop"); // 대기 상태로 전환됩니다.

결과:

달리기 상태로 전환됩니다.
원거리 공격을 실행합니다.
대기 상태로 전환됩니다.

이 예제에서 각 상태는 서로 다른 공격 전략을 사용하도록 설계되었습니다. 대기 상태에서는 근접 공격, 달리기 상태에서는 원거리 공격을 사용하며, 상태에 따라 적절한 전략을 선택하여 행동할 수 있습니다. 이는 상태 패턴과 전략 패턴을 결합함으로써 상태 전환과 전략 선택을 유연하게 결합한 예입니다.


4. 상태 패턴과 다른 디자인 패턴의 결합

상태 패턴은 전략 패턴 외에도 다양한 디자인 패턴과 결합할 수 있습니다. 예를 들어, 템플릿 메서드 패턴(Template Method Pattern)과 결합하여 상태에 따른 동작의 공통적인 부분을 템플릿 메서드로 정의하고, 구체적인 동작만 각 상태에서 구현할 수 있습니다.

4.1 템플릿 메서드 패턴과 결합

템플릿 메서드 패턴을 사용하면 상태 전환과 관련된 공통 로직을 상위 클래스에 정의하고, 각 상태에서 세부 동작만 구현하도록 만들 수 있습니다. 다음은 상태 패턴과 템플릿 메서드 패턴을 결합한 예제입니다.

abstract class CharacterStateTemplate {
  handleInput(character: GameCharacter, input: string): void {
    this.commonLogic();
    this.stateSpecificLogic(character, input);
  }

  protected commonLogic(): void {
    console.log("공통 로직 실행 중...");
  }

  protected abstract stateSpecificLogic(
    character: GameCharacter,
    input: string
  ): void;
}

class IdleStateTemplate extends CharacterStateTemplate {
  protected stateSpecificLogic(character: GameCharacter, input: string): void {
    if (input === "run") {
      console.log("달리기 상태로 전환됩니다.");
      character.setState(new RunningStateTemplate());
    } else {
      console.log("대기 상태입니다.");
    }
  }
}

class RunningStateTemplate extends CharacterStateTemplate {
  protected stateSpecificLogic(character: GameCharacter, input: string): void {
    if (input === "stop") {
      console.log("대기 상태로 전환됩니다.");
      character.setState(new IdleStateTemplate());
    } else {
      console.log("달리는 중입니다.");
    }
  }
}

이 예제에서 템플릿 메서드를 사용하여 공통 로직은 commonLogic()에서 처리하고, 상태별 세부 로직만 각 상태 클래스에서 구현합니다. 이를 통해 중복 코드를 줄이고, 상태 전환 로직을 간결하게 관리할 수 있습니다.


5. 상태 패턴의 장점과 단점

상태 패턴은 매우 유용한 디자인 패턴이지만, 몇 가지 단점도 존재합니다. 상태 패턴을 사용할 때는 장단점을 고려하여 적절한 상황에서 적용하는 것이 중요합니다.

5.1 장점

  1. 복잡한 조건문 제거: 상태별 행동을 상태 객체로 분리함으로써 복잡한 조건문을 제거할 수 있습니다.
  2. 확장성: 새로운 상태를 추가하거나 상태별 동작을 변경할 때 기존 코드를 수정하지 않고 확장할 수 있습니다.
  3. 유지보수성 향상: 각 상태별 동작이 명확하게 분리되어 있으므로, 유지보수와 수정이 용이합니다.

5.2 단점

  1. 객체 수 증가: 상태마다 객체를 생성하므로 객체 수가 증가하고 코드가 복잡해질 수 있습니다.
  2. 상태 전환 로직의 복잡성: 상태 전환이 많아질 경우, 상태 전환을 관리하는 로직이 복잡해질 수 있습니다.
  3. 테스트 복잡성: 상태별로 많은 테스트 케이스가 필요해질 수 있으며, 상태 전환을 포함한 테스트 시나리오를 잘 관리해야 합니다.

6. 결론

이번 글에서는 상태 패턴의 확장과 변형 가능성에 대해 살펴보았습니다. 함수형 프로그래밍에서의 상태 패턴 구현, 상태 패턴과 전략 패턴의 결합, 그리고 템플릿 메서드 패턴과의 결합을 통해 상태 패턴을 더욱 유연하게 사용할 수 있음을 확인했습니다. 상태 패턴은 다양한 상황에서 적용할 수 있으며, 이를 적절히 변형하여 더 효율적인 설계를 구현할 수 있습니다.

다음 글에서는 상태 패턴의 장단점과, 상태 패턴을 실무에서 사용하는 최적의 방법에 대해 다룰 예정입니다.