본문 바로가기
디자인패턴

상태 패턴의 구체적인 구현(State Pattern)

by 대박플머 2024. 10. 17.

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

 

이번 글에서는 상태 패턴을 TypeScript로 구체적으로 구현하는 방법을 다룰 것입니다. 상태 패턴은 객체의 상태에 따라 다른 행동을 수행하게 하는 패턴이므로, 이를 구현하기 위해 인터페이스와 상태 전환 로직을 단계적으로 설명하고, 실용적인 예제를 통해 상태 패턴을 더 깊이 있게 이해해 보겠습니다.

 


1. 상태 패턴의 핵심 개념

상태 패턴(State Pattern)의 가장 큰 특징은 객체의 상태가 바뀔 때 객체의 동작이 달라진다는 점입니다. 객체의 행동이 그 내부 상태에 따라 변화하고, 이 상태 변화는 상태 객체 간의 전환을 통해 이루어집니다. 상태 패턴은 복잡한 조건문을 제거하고, 상태에 따른 행동을 각각의 상태 객체로 분리함으로써 코드의 가독성과 확장성을 높입니다.

상태 패턴은 크게 세 가지 요소로 구성됩니다.

  • Context(문맥): 현재 상태를 관리하고, 상태 전환을 실행하며, 상태에 따른 동작을 외부에서 요청받는 역할을 합니다.
  • State(상태 인터페이스): 상태별로 공통된 동작을 정의하는 인터페이스입니다. 각 상태는 이 인터페이스를 구현하여 고유한 행동을 정의합니다.
  • ConcreteState(구체적인 상태 클래스): 상태 인터페이스를 구현한 각 상태의 구체적인 클래스입니다. 이 클래스는 상태에 따른 구체적인 동작을 정의하고, 상태 전환을 담당합니다.

2. 상태 패턴을 TypeScript로 구현하기

이제 상태 패턴을 TypeScript로 단계별로 구현해 보겠습니다. 예제를 통해 ATM 기기에서 카드를 삽입하고 핀 번호를 입력하고, 돈을 인출하는 과정에서 상태가 어떻게 전환되는지 살펴보겠습니다.

2.1 상태 인터페이스 정의

우선, 상태 패턴의 기본 구조를 구성하는 상태 인터페이스를 정의해야 합니다. 각 상태는 이 인터페이스를 구현하여 고유한 행동을 수행합니다.

interface ATMState {
  insertCard(): void;
  ejectCard(): void;
  enterPin(pin: number): void;
  withdrawCash(amount: number): void;
}

ATMState 인터페이스는 ATM 기기에서 수행할 수 있는 네 가지 행동을 정의합니다. 각 상태에서 이 행동들을 다르게 구현할 수 있습니다.

2.2 문맥 클래스 정의

다음으로, 상태 객체를 관리하는 문맥 클래스를 정의합니다. 이 클래스는 현재 상태를 보유하고, 각 상태에 따라 행동을 위임합니다.

class ATMMachine {
  private currentState: ATMState;
  private hasCardState: ATMState;
  private noCardState: ATMState;
  private hasCorrectPinState: ATMState;
  private outOfCashState: ATMState;
  private atmBalance: number = 1000; // 초기 ATM 잔액

  constructor() {
    this.hasCardState = new HasCardState(this);
    this.noCardState = new NoCardState(this);
    this.hasCorrectPinState = new HasCorrectPinState(this);
    this.outOfCashState = new OutOfCashState(this);

    this.currentState = this.noCardState; // 초기 상태는 카드가 없는 상태
  }

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

  getHasCardState(): ATMState {
    return this.hasCardState;
  }

  getNoCardState(): ATMState {
    return this.noCardState;
  }

  getHasCorrectPinState(): ATMState {
    return this.hasCorrectPinState;
  }

  getOutOfCashState(): ATMState {
    return this.outOfCashState;
  }

  getBalance(): number {
    return this.atmBalance;
  }

  withdraw(amount: number): void {
    this.atmBalance -= amount;
  }

  // 상태에 따라 행동을 위임
  insertCard(): void {
    this.currentState.insertCard();
  }

  ejectCard(): void {
    this.currentState.ejectCard();
  }

  enterPin(pin: number): void {
    this.currentState.enterPin(pin);
  }

  withdrawCash(amount: number): void {
    this.currentState.withdrawCash(amount);
  }
}

ATMMachine 클래스는 현재 ATM의 상태를 저장하고 있으며, 각 상태별로 적절한 행동을 위임하는 역할을 합니다. 초기 상태는 카드가 없는 상태(noCardState)로 설정됩니다.

2.3 구체적인 상태 클래스 구현

이제 각 상태에 따라 달라지는 행동을 정의하는 구체적인 상태 클래스를 작성합니다.

2.3.1 카드가 없는 상태(NoCardState)

class NoCardState implements ATMState {
  private atmMachine: ATMMachine;

  constructor(atmMachine: ATMMachine) {
    this.atmMachine = atmMachine;
  }

  insertCard(): void {
    console.log("카드를 삽입했습니다.");
    this.atmMachine.setState(this.atmMachine.getHasCardState());
  }

  ejectCard(): void {
    console.log("카드가 없습니다. 카드를 삽입해 주세요.");
  }

  enterPin(pin: number): void {
    console.log("카드가 없습니다. 카드를 먼저 삽입해 주세요.");
  }

  withdrawCash(amount: number): void {
    console.log("카드가 없습니다. 카드를 먼저 삽입해 주세요.");
  }
}

카드가 없는 상태에서 카드를 삽입하면 상태가 HasCardState로 전환됩니다. 이 외의 행동은 모두 카드가 없다는 메시지를 출력합니다.

2.3.2 카드가 있는 상태(HasCardState)

class HasCardState implements ATMState {
  private atmMachine: ATMMachine;

  constructor(atmMachine: ATMMachine) {
    this.atmMachine = atmMachine;
  }

  insertCard(): void {
    console.log("이미 카드를 삽입했습니다.");
  }

  ejectCard(): void {
    console.log("카드를 반환합니다.");
    this.atmMachine.setState(this.atmMachine.getNoCardState());
  }

  enterPin(pin: number): void {
    if (pin === 1234) {
      // 핀 번호가 맞는 경우
      console.log("핀 번호가 확인되었습니다.");
      this.atmMachine.setState(this.atmMachine.getHasCorrectPinState());
    } else {
      console.log("잘못된 핀 번호입니다. 카드를 반환합니다.");
      this.atmMachine.setState(this.atmMachine.getNoCardState());
    }
  }

  withdrawCash(amount: number): void {
    console.log("핀 번호를 입력해 주세요.");
  }
}

카드가 삽입된 상태에서는 핀 번호 입력을 대기합니다. 핀이 맞으면 상태가 HasCorrectPinState로 전환됩니다.

2.3.3 올바른 핀 번호가 입력된 상태(HasCorrectPinState)

class HasCorrectPinState implements ATMState {
  private atmMachine: ATMMachine;

  constructor(atmMachine: ATMMachine) {
    this.atmMachine = atmMachine;
  }

  insertCard(): void {
    console.log("이미 카드를 삽입했습니다.");
  }

  ejectCard(): void {
    console.log("카드를 반환합니다.");
    this.atmMachine.setState(this.atmMachine.getNoCardState());
  }

  enterPin(pin: number): void {
    console.log("이미 핀 번호가 확인되었습니다.");
  }

  withdrawCash(amount: number): void {
    if (this.atmMachine.getBalance() >= amount) {
      console.log(`${amount}원을 인출합니다.`);
      this.atmMachine.withdraw(amount);

      if (this.atmMachine.getBalance() === 0) {
        console.log("ATM에 잔액이 없습니다.");
        this.atmMachine.setState(this.atmMachine.getOutOfCashState());
      } else {
        this.atmMachine.setState(this.atmMachine.getNoCardState());
      }
    } else {
      console.log("잔액이 부족합니다.");
      this.atmMachine.setState(this.atmMachine.getNoCardState());
    }
  }
}

올바른 핀 번호가 입력되면 사용자는 돈을 인출할 수 있습니다. 잔액이 부족하면 상태가 다시 NoCardState로 전환됩니다.

2.3.4 잔액이 없는 상태(OutOfCashState)

class OutOfCashState implements ATMState {
  private atmMachine: ATMMachine;

  constructor(atmMachine: ATMMachine) {
    this.atmMachine = atmMachine;
  }

  insertCard(): void {
    console.log("ATM에 잔액이 없습니다. 카드를 반환합니다.");
  }

  ejectCard(): void {
    console.log("카드가 없습니다.");
  }

  enterPin(pin: number): void {
    console.log("ATM에 잔액이 없습니다.");
  }

  withdrawCash(amount: number): void {
    console.log("ATM에 잔액이 없습니다.");
  }
}

ATM에 잔액이 없으면 더 이상 인출할 수 없으며, 상태는 OutOfCashState로 고정됩니다.


3. 상태 전환 테스트

상태 패턴이 정상적으로 작동하는지 테스트하기 위해 다음과 같은 코드를 작성할 수 있습니다.

const atmMachine = new ATMMachine();

atmMachine.insertCard();
atmMachine.enterPin(1234);
atmMachine.withdrawCash(500);

atmMachine.insertCard();
atmMachine.enterPin(1234);
atmMachine.withdrawCash(600); // 잔액 부족

atmMachine.insertCard(); // ATM에 잔액이 없는 상태

출력 결과는 다음과 같습니다:

카드를 삽입했습니다.
핀 번호가 확인되었습니다.
500원을 인출합니다.
카드를 삽입했습니다.
핀 번호가 확인되었습니다.
잔액이 부족합니다.
ATM에 잔액이 없습니다. 카드를 반환합니다.

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

장점

  1. 복잡한 조건문 제거: 상태 전환 로직을 상태 객체로 분리하여 복잡한 조건문을 제거할 수 있습니다.
  2. 유지보수성 향상: 상태에 따른 동작을 각각의 객체로 분리하므로, 상태별 동작을 수정하거나 확장할 때 코드 수정 범위가 좁아집니다.
  3. 확장성: 새로운 상태를 추가하기 쉬워 확장성이 뛰어납니다.

단점

  1. 객체 수 증가: 상태마다 객체를 생성하므로 객체 수가 증가하고, 코드가 다소 복잡해질 수 있습니다.
  2. 상태 전환 관리의 복잡성: 많은 상태 전환이 필요한 경우, 상태 전환을 관리하는 로직이 복잡해질 수 있습니다.

5. 결론

이번 글에서는 상태 패턴을 TypeScript로 구현하는 방법을 다뤘습니다. 상태 패턴은 복잡한 상태 전환 로직을 간결하게 관리할 수 있는 강력한 도구입니다. ATM 기기의 상태 전환 예제를 통해 상태 패턴의 구체적인 구현 방법을 살펴보았으며, 이를 통해 코드의 가독성 및 유지보수성을 높일 수 있음을 확인했습니다.


상태 패턴의 응용 사례

이번 글에서는 상태 패턴(State Pattern)이 실무에서 어떻게 응용되는지, 다양한 사례를 통해 알아보겠습니다. 상태 패턴은 객체의 상태에 따라 동작을 변경하는 특성 때문에 다양한 시나리오에서 유용하게 사용됩니다. 특히 게임 개발, 웹 애플리케이션의 사용자 인터페이스(UI) 관리, 그리고 금융 시스템에서의 트랜잭션 처리 등에서 자주 활용됩니다. 상태 패턴의 응용 사례를 통해 패턴의 유용성을 깊이 이해해 보도록 하겠습니다.


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

상태 패턴은 객체가 내부 상태에 따라 다른 동작을 수행하도록 하는 디자인 패턴입니다. 이 패턴의 주요 목표는 조건문을 사용하는 대신, 각 상태를 별도의 객체로 관리하여 상태 변화에 따라 유연하게 행동을 변경하는 것입니다. 상태 패턴은 특히 복잡한 상태 전환 로직이 있는 시스템에서 효과적입니다.

상태 패턴의 주요 요소:

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

이 기본 개념을 토대로 실무에서 상태 패턴이 어떻게 사용되는지 살펴보겠습니다.


2. 게임 개발에서의 상태 패턴 활용

게임 개발에서는 상태 패턴이 매우 자주 사용됩니다. 특히 캐릭터의 상태(Idle, Running, Attacking 등)를 관리하는 데 유용합니다. 캐릭터가 어떤 상태에 있는지에 따라 동작이 달라져야 하므로, 상태 패턴을 통해 이러한 상태 변화를 깔끔하게 관리할 수 있습니다.

2.1 캐릭터의 상태 관리 예제

아래 예제는 게임 캐릭터의 상태를 관리하는 코드입니다. 캐릭터는 Idle(대기), Running(달리기), Attacking(공격) 상태를 가질 수 있으며, 각 상태에 따라 다른 행동을 수행합니다.

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());
    } else {
      console.log("캐릭터는 대기 상태입니다.");
    }
  }
}

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());
    } else {
      console.log("캐릭터는 달리고 있습니다.");
    }
  }
}

class AttackingState implements CharacterState {
  handleInput(character: GameCharacter, input: string): void {
    if (input === "stop") {
      console.log("캐릭터가 대기 상태로 전환됩니다.");
      character.setState(new IdleState());
    } else if (input === "run") {
      console.log("캐릭터가 달리기 상태로 전환됩니다.");
      character.setState(new RunningState());
    } else {
      console.log("캐릭터는 공격하고 있습니다.");
    }
  }
}

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);
  }
}

위 코드는 캐릭터가 Idle, Running, Attacking 상태 중 하나에 있을 때 어떤 행동을 해야 하는지를 정의합니다. 상태 패턴을 사용하면 상태별 동작을 각각의 클래스에서 정의하므로, 상태가 늘어나도 코드가 복잡해지지 않고 확장하기 쉽습니다.

2.2 상태 패턴을 적용한 게임 캐릭터의 행동

이제 상태 패턴을 적용한 캐릭터가 어떻게 동작하는지 살펴보겠습니다.

const character = new GameCharacter();

character.handleInput("run"); // 캐릭터가 달리기 상태로 전환됩니다.
character.handleInput("attack"); // 캐릭터가 공격 상태로 전환됩니다.
character.handleInput("stop"); // 캐릭터가 대기 상태로 전환됩니다.

결과:

캐릭터가 달리기 상태로 전환됩니다.
캐릭터가 공격 상태로 전환됩니다.
캐릭터가 대기 상태로 전환됩니다.

상태 패턴을 통해 캐릭터의 상태 변화를 간결하게 관리할 수 있음을 알 수 있습니다. 게임 개발에서 캐릭터나 NPC(Non-Playable Character)의 상태 변화를 관리할 때 상태 패턴은 매우 유용합니다.


3. 웹 애플리케이션에서의 상태 패턴 응용

상태 패턴은 웹 애플리케이션의 사용자 인터페이스(UI) 상태 관리에서도 자주 사용됩니다. 웹 애플리케이션에서는 사용자의 상호작용에 따라 UI 상태가 변할 수 있으며, 이러한 상태 변화를 관리하는 데 상태 패턴을 사용할 수 있습니다.

3.1 로그인 상태 관리 예제

로그인 상태를 관리하는 웹 애플리케이션을 예로 들어보겠습니다. 사용자는 로그아웃, 로그인 중, 로그인됨 상태를 가지며, 상태에 따라 UI가 달라집니다.

interface LoginState {
  login(context: LoginContext): void;
  logout(context: LoginContext): void;
}

class LoggedOutState implements LoginState {
  login(context: LoginContext): void {
    console.log("사용자가 로그인 중입니다.");
    context.setState(new LoggingInState());
  }

  logout(context: LoginContext): void {
    console.log("사용자는 이미 로그아웃 상태입니다.");
  }
}

class LoggingInState implements LoginState {
  login(context: LoginContext): void {
    console.log("이미 로그인 처리 중입니다.");
  }

  logout(context: LoginContext): void {
    console.log(
      "로그인 처리 중 로그아웃 요청을 받았습니다. 로그아웃 상태로 전환됩니다."
    );
    context.setState(new LoggedOutState());
  }
}

class LoggedInState implements LoginState {
  login(context: LoginContext): void {
    console.log("이미 로그인된 상태입니다.");
  }

  logout(context: LoginContext): void {
    console.log("사용자가 로그아웃되었습니다.");
    context.setState(new LoggedOutState());
  }
}

class LoginContext {
  private state: LoginState;

  constructor() {
    this.state = new LoggedOutState(); // 초기 상태는 로그아웃 상태
  }

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

  login(): void {
    this.state.login(this);
  }

  logout(): void {
    this.state.logout(this);
  }
}

위 코드는 로그인 상태를 관리하는 시스템을 상태 패턴으로 구현한 예입니다. 사용자는 로그인 상태에 따라 login() 또는 logout() 동작을 할 수 있으며, 상태에 따라 적절한 동작이 실행됩니다.

3.2 로그인 상태 관리 시나리오

상태 패턴을 적용한 로그인 상태 관리의 예는 다음과 같습니다.

const loginContext = new LoginContext();

loginContext.login(); // 사용자가 로그인 중입니다.
loginContext.logout(); // 로그인 처리 중 로그아웃 요청을 받았습니다. 로그아웃 상태로 전환됩니다.
loginContext.login(); // 사용자가 로그인 중입니다.
loginContext.login(); // 이미 로그인 처리 중입니다.
loginContext.logout(); // 사용자는 이미 로그아웃 상태입니다.

결과:

사용자가 로그인 중입니다.
로그인 처리 중 로그아웃 요청을 받았습니다. 로그아웃 상태로 전환됩니다.
사용자가 로그인 중입니다.
이미 로그인 처리 중입니다.
사용자는 이미 로그아웃 상태입니다.

이 예제에서는 상태 패턴을 사용하여 로그인과 로그아웃 상태를 관리하고, 각 상태에 따라 적절한 동작을 처리하도록 만들었습니다. 상태 패턴을 사용하면 UI 상태가 변할 때마다 이를 상태 객체로 분리하여 관리할 수 있어 유지보수와 확장성이 높아집니다.


4. 금융 시스템에서의 상태 패턴 응용

금융 시스템에서 트랜잭션 상태를 관리하는 것도 상태 패턴의 중요한 응용 사례 중 하나입니다. 예를 들어, 결제 시스템에서는 트랜잭션이 여러 상태를 거치게 되며, 이 상태에 따라 결제 승인, 실패, 취소 등의 처리가 이루어집니다.

4.1 결제 트랜잭션 상태 관리 예제

아래 코드는 결제 트랜잭션의 상태를 관리하는 예입니다. 트랜잭션은 Created, Pending, Completed, Cancelled 상태를 가지며, 상태에 따라 다른 동작을 수행합니다.

interface TransactionState {
  process(context: TransactionContext): void;
  cancel(context: TransactionContext): void;
}

class CreatedState implements TransactionState {
  process(context: TransactionContext): void {
    console.log("트랜잭션이 승인 대기 중입니다.");
    context.setState(new PendingState());
  }

  cancel(context: TransactionContext): void {
    console.log("트랜잭션이 취소되었습니다.");
    context.setState(new CancelledState());
  }
}

class PendingState implements TransactionState {
  process(context: TransactionContext): void {
    console.log("트랜잭션이 완료되었습니다.");
    context.setState(new CompletedState());
  }

  cancel(context: TransactionContext): void {
    console.log("트랜잭션이 처리 중이므로 취소할 수 없습니다.");
  }
}

class CompletedState implements TransactionState {
  process(context: TransactionContext): void {
    console.log("이미 완료된 트랜잭션입니다.");
  }

  cancel(context: TransactionContext): void {
    console.log("이미 완료된 트랜잭션은 취소할 수 없습니다.");
  }
}

class CancelledState implements TransactionState {
  process(context: TransactionContext): void {
    console.log("취소된 트랜잭션입니다. 처리할 수 없습니다.");
  }

  cancel(context: TransactionContext): void {
    console.log("이미 취소된 트랜잭션입니다.");
  }
}

class TransactionContext {
  private state: TransactionState;

  constructor() {
    this.state = new CreatedState(); // 초기 상태는 생성된 상태
  }

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

  process(): void {
    this.state.process(this);
  }

  cancel(): void {
    this.state.cancel(this);
  }
}

4.2 결제 트랜잭션 상태 전환 시나리오

const transaction = new TransactionContext();

transaction.process(); // 트랜잭션이 승인 대기 중입니다.
transaction.process(); // 트랜잭션이 완료되었습니다.
transaction.cancel(); // 이미 완료된 트랜잭션은 취소할 수 없습니다.

결과:

트랜잭션이 승인 대기 중입니다.
트랜잭션이 완료되었습니다.
이미 완료된 트랜잭션은 취소할 수 없습니다.

트랜잭션 상태 관리에서 상태 패턴을 사용하면 각 상태별로 트랜잭션 처리 로직을 분리하여 관리할 수 있습니다. 트랜잭션이 어떤 상태에 있는지에 따라 처리 방식이 달라지며, 상태 패턴을 통해 이를 깔끔하게 관리할 수 있습니다.


5. 상태 패턴의 응용 장점

상태 패턴을 다양한 실무 사례에 적용하면서 얻을 수 있는 주요 장점은 다음과 같습니다.

  1. 복잡한 상태 전환 로직의 간결화: 상태별로 로직을 분리하여 조건문을 최소화하고, 상태 전환이 명확해집니다.
  2. 확장성: 새로운 상태를 쉽게 추가할 수 있으며, 기존 로직에 최소한의 영향만 미칩니다.
  3. 상태에 따른 행동의 분리: 상태와 행동이 명확하게 분리되므로, 유지보수가 용이하고 가독성이 높아집니다.
  4. 상태 전환의 명시적 관리: 상태 전환이 각 상태 객체에서 명시적으로 이루어지므로, 상태 관리가 더 직관적이고 투명해집니다.

6. 결론

이번 글에서는 상태 패턴이 실무에서 어떻게 사용되는지, 다양한 응용 사례를 통해 살펴보았습니다. 게임 캐릭터의 상태 관리, 웹 애플리케이션의 로그인 상태 관리, 금융 시스템에서의 트랜잭션 상태 관리 등에서 상태 패턴이 유용하게 적용될 수 있습니다. 이러한 사례들을 통해 상태 패턴의 장점과 실무에서의 유용성을 확인할 수 있었습니다.