디자인 패턴상태 패턴

[ Programming > Design Pattern ]

[디자인 패턴] 상태 패턴

 Carrot Yoon
 2025-10-10
 4

상태 패턴(State Pattern)

상태 패턴을 구현해서 객체의 상태에 따라 다르게 동작하는 코드를 깔끔하게 관리하는 방법을 알려드려요. 이 패턴으로 복잡한 조건문 없이도 상태별로 다른 행동을 쉽게 구현할 수 있어요.

상태 패턴이 왜 필요한가요?

실무에서 이런 상황을 자주 마주칠 수 있어요. 커머스 플랫폼에서 주문 상태를 관리하는데, 주문 상태에 따라 할 수 있는 행동이 달라요. 예를 들어 배송 중인 주문은 취소할 수 없지만, 배송 준비 중인 주문은 취소할 수 있죠.

조건문으로 상태 관리하는 코드

class Order {  constructor(orderId) {    this.orderId = orderId;    this.state = 'ORDERED'; // 주문 완료  }    cancel() {    if (this.state === 'ORDERED') {      console.log('주문이 취소되었습니다.');      this.state = 'CANCELLED';    } else if (this.state === 'PREPARING') {      console.log('배송 준비 중인 주문이 취소되었습니다.');      this.state = 'CANCELLED';    } else if (this.state === 'SHIPPING') {      console.log('배송 중인 주문은 취소할 수 없습니다.');    } else if (this.state === 'DELIVERED') {      console.log('배송 완료된 주문은 취소할 수 없습니다. 환불을 신청해주세요.');    } else if (this.state === 'CANCELLED') {      console.log('이미 취소된 주문입니다.');    }  }    ship() {    if (this.state === 'ORDERED') {      console.log('배송 준비를 먼저 해주세요.');    } else if (this.state === 'PREPARING') {      console.log('배송을 시작합니다.');      this.state = 'SHIPPING';    } else if (this.state === 'SHIPPING') {      console.log('이미 배송 중입니다.');    } else if (this.state === 'DELIVERED') {      console.log('이미 배송 완료된 주문입니다.');    } else if (this.state === 'CANCELLED') {      console.log('취소된 주문은 배송할 수 없습니다.');    }  }    prepare() {    if (this.state === 'ORDERED') {      console.log('배송 준비를 시작합니다.');      this.state = 'PREPARING';    } else if (this.state === 'PREPARING') {      console.log('이미 배송 준비 중입니다.');    } else if (this.state === 'SHIPPING') {      console.log('이미 배송이 시작되었습니다.');    } else if (this.state === 'DELIVERED') {      console.log('이미 배송 완료된 주문입니다.');    } else if (this.state === 'CANCELLED') {      console.log('취소된 주문입니다.');    }  }    deliver() {    if (this.state === 'SHIPPING') {      console.log('배송이 완료되었습니다.');      this.state = 'DELIVERED';    } else {      console.log('배송 중인 주문만 배송 완료 처리할 수 있습니다.');    }  }    refund() {    if (this.state === 'DELIVERED') {      console.log('환불이 처리되었습니다.');      this.state = 'REFUNDED';    } else {      console.log('배송 완료된 주문만 환불할 수 있습니다.');    }  }}

Clinet 코드

const order = new Order('ORDER-001');order.prepare();  // 배송 준비를 시작합니다.order.ship();     // 배송을 시작합니다.order.cancel();   // 배송 중인 주문은 취소할 수 없습니다.order.deliver();  // 배송이 완료되었습니다.order.refund();   // 환불이 처리되었습니다.

위 코드의 문제점이 보이시나요?

문제점

  • 모든 메서드에 조건문이 가득해요 - 상태가 추가될 때마다 모든 메서드를 수정해야 해요

  • 코드 중복이 많아요 - 비슷한 조건문이 여러 메서드에 반복돼요

  • 상태 전환 로직이 분산돼요 - 상태 변경 규칙을 한눈에 파악하기 어려워요

  • 확장이 어려워요 - 새로운 상태(예: 부분 환불, 교환 중)를 추가하려면 모든 메서드를 수정해야 해요

  • 테스트가 복잡해요 - 각 메서드마다 모든 상태를 테스트해야 해요

그러면 위와 같은 문제점을 어떻게 해결할 수 있을까요? 바로 상태 패턴을 사용하면 각 상태를 독립적인 클래스로 만들어 조건문 없이 깔끔하게 관리할 수 있어요. 새로운 상태가 추가되어도 기존 코드를 수정하지 않고 새로운 상태 클래스만 추가하면 돼요.

상태 패턴으로 해결하기

상태 패턴을 적용하면 각 상태를 독립적인 클래스로 분리해서 관리할 수 있어요.

Order의 상태 인터페이스와 구현 클래스(State)

상태를 구성으로 가지는 Order 클래스(Context)

OrderState 인터페이스

// 상태 인터페이스class OrderState {  cancel(order) {    console.log('이 상태에서는 취소할 수 없습니다.');  }    prepare(order) {    console.log('이 상태에서는 배송 준비를 할 수 없습니다.');  }    ship(order) {    console.log('이 상태에서는 배송을 시작할 수 없습니다.');  }    deliver(order) {    console.log('이 상태에서는 배송 완료 처리를 할 수 없습니다.');  }    refund(order) {    console.log('이 상태에서는 환불할 수 없습니다.');  }    getStateName() {    return 'Unknown';  }}

OrderState 구현 클래스

// 주문 완료 상태class OrderedState extends OrderState {  cancel(order) {    console.log('주문이 취소되었습니다.');    order.setState(new CancelledState());  }    prepare(order) {    console.log('배송 준비를 시작합니다.');    order.setState(new PreparingState());  }    getStateName() {    return '주문 완료';  }}// 배송 준비 중 상태class PreparingState extends OrderState {  cancel(order) {    console.log('배송 준비 중인 주문이 취소되었습니다.');    order.setState(new CancelledState());  }    ship(order) {    console.log('배송을 시작합니다.');    order.setState(new ShippingState());  }    getStateName() {    return '배송 준비 중';  }}// 배송 중 상태class ShippingState extends OrderState {  cancel(order) {    console.log('배송 중인 주문은 취소할 수 없습니다.');  }    deliver(order) {    console.log('배송이 완료되었습니다.');    order.setState(new DeliveredState());  }    getStateName() {    return '배송 중';  }}// 배송 완료 상태class DeliveredState extends OrderState {  cancel(order) {    console.log('배송 완료된 주문은 취소할 수 없습니다. 환불을 신청해주세요.');  }    refund(order) {    console.log('환불이 처리되었습니다.');    order.setState(new RefundedState());  }    getStateName() {    return '배송 완료';  }}// 취소 상태class CancelledState extends OrderState {  cancel(order) {    console.log('이미 취소된 주문입니다.');  }    getStateName() {    return '취소됨';  }}// 환불 상태class RefundedState extends OrderState {  refund(order) {    console.log('이미 환불 처리된 주문입니다.');  }    getStateName() {    return '환불 완료';  }}

class Order {  constructor(orderId) {    this.orderId = orderId;    this.state = new OrderedState(); // 초기 상태  }    setState(state) {    this.state = state;    console.log(`[상태 변경] → ${state.getStateName()}`);  }    cancel() {    this.state.cancel(this);  }    prepare() {    this.state.prepare(this);  }    ship() {    this.state.ship(this);  }    deliver() {    this.state.deliver(this);  }    refund() {    this.state.refund(this);  }    getStatus() {    return this.state.getStateName();  }}

Client의 코드

// === 정상 주문 플로우 ===const order1 = new Order('ORDER-001');order1.prepare();  // 배송 준비를 시작합니다.order1.ship();     // 배송을 시작합니다.order1.cancel();   // 배송 중인 주문은 취소할 수 없습니다.order1.deliver();  // 배송이 완료되었습니다.order1.refund();   // 환불이 처리되었습니다.// === 주문 후 바로 취소하는 경우 ===const order2 = new Order('ORDER-002');order2.cancel();   // 주문이 취소되었습니다.order2.prepare();  // 이 상태에서는 배송 준비를 할 수 없습니다.// === 배송 준비 중 취소하는 경우 ===const order3 = new Order('ORDER-003');order3.prepare();  // 배송 준비를 시작합니다.order3.cancel();   // 배송 준비 중인 주문이 취소되었습니다.

어때요? 복잡한 조건문이 사라지고 각 상태별로 독립적인 클래스가 관리하니 훨씬 깔끔해졌죠?

상태 패턴 클래스 다이어그램

상태 패턴의 클래스 다이어그램이 어떻게 되는지 보여드릴게요.

상태 패턴 다이어그램.webp

Context는 현재 상태 객체를 참조하고, 모든 요청을 현재 상태 객체에 위임해요. 각 상태 클래스는 State 인터페이스를 구현하며, 자신이 처리할 수 있는 행동만 구체적으로 정의해요. 전략 패턴의 다이어그램과 같지만 전략 패턴은 Context가 만들어질때 Strategy가 결정되어 함께 주입되지만 상태 패턴에서는 초기 상태를 설정해주는 부분은 같지만, 이후에는 State나 Context에서 상태를 알아서 변경해요. 즉, 목적이 달르다고 볼 수 있어요.

상태 패턴 vs 조건문 방식 비교

구분

상태 패턴

조건문 방식

코드 구조

상태별로 클래스 분리, 깔끔함

모든 메서드에 조건문 가득

확장성

새 상태 클래스만 추가하면 됨

모든 메서드에 조건문 추가 필요

유지보수

상태별로 독립적으로 수정 가능

한 곳 수정 시 다른 곳도 영향

가독성

상태 전환 로직이 명확함

조건문이 복잡하고 파악 어려움

테스트

상태 클래스별로 독립적 테스트

모든 조건 조합을 테스트해야 함

OCP 원칙

기존 코드 수정 없이 확장 가능

기존 코드를 계속 수정해야 함

마무리

이렇게 상태 패턴을 통해서 객체의 상태에 따라 달라지는 행동을 깔끔하게 관리할 수 있게 만들어 봤습니다. 이렇게 함으로써 복잡한 조건문이 사라지고, 각 상태별로 독립적인 클래스가 책임을 지니 단일 책임 원칙(SRP)을 잘 따르게 됩니다. 또한 새로운 상태가 추가되어도 기존 코드를 수정하지 않고 새로운 상태 클래스만 추가하면 되니 개방-폐쇄 원칙(OCP)도 잘 지키게 돼요.

그러면 항상 상태 관리할 때 패턴을 사용하면 될까요? 아니요. 상태가 2-3개 정도로 단순하거나, 상태 전환이 거의 일어나지 않거나, 상태별 행동 차이가 거의 없다면 굳이 상태 패턴을 쓸 필요가 없어요. 간단한 조건문이나 상태 플래그로 충분합니다. (오버 엔지니어링은 오히려 코드 파악을 힘들게 만듭니다.)

상태 패턴의 진정한 가치는 상태가 많고 복잡하며, 각 상태마다 다른 행동을 해야 하는 경우에 빛을 발해요. 이를 통해 코드의 복잡도를 낮추고, 유지보수성과 확장성을 크게 향상시킬 수 있습니다.