← Posts

GoF · Behavioral · State

State

상태에 따라 행동이 갈릴 때. 상태를 객체로 만들어 거대한 조건문을 다형성으로 바꾸는 행위 패턴.

목차
  1. 상태마다 행동이 달라 조건문이 커진다
  2. 상태 패턴 = 상태를 객체로 만든다
  3. State 인터페이스로 행동을 정의한다
  4. 컨텍스트가 현재 상태에 위임한다
  5. 상태 전이는 상태가 스스로 한다
  6. 조건문이 다형성으로 사라진다
  7. 전략과 무엇이 다른가
  8. 실무: 상태 머신
  9. 정리

상태마다 행동이 달라 조건문이 커진다

주문 하나가 여러 상태를 거친다. 결제대기, 결제완료, 배송중, 완료, 취소. 그리고 같은 동작이라도 어느 상태냐에 따라 결과가 다르다. 결제완료 상태에서 배송을 걸면 배송중으로 넘어가지만, 결제대기 상태에서 배송을 걸면 안 된다.

java
public class Order {
    private String state;   // "PENDING", "PAID", "SHIPPING", ...

    public void ship() {
        if (state.equals("PAID")) {
            state = "SHIPPING";
        } else if (state.equals("PENDING")) {
            throw new IllegalStateException("결제 전엔 배송 불가");
        } else if (state.equals("SHIPPING")) {
            throw new IllegalStateException("이미 배송 중");
        }
        // ...
    }

    public void cancel() {
        if (state.equals("PENDING")) {
            state = "CANCELED";
        } else if (state.equals("PAID")) {
            state = "CANCELED";   // 환불 처리도
        } else if (state.equals("SHIPPING")) {
            throw new IllegalStateException("배송 중엔 취소 불가");
        }
        // ...
    }
}

ship(), cancel(), deliver()… 메서드마다 똑같은 상태 분기가 되풀이된다. 상태를 하나 추가하면(예: “환불중”) 이 모든 메서드의 if를 다 찾아 손봐야 한다. 한 상태의 규칙이 여러 메서드에 흩어져 있어, “결제완료 상태가 무슨 동작을 허용하는가”를 한눈에 볼 수도 없다.

상태 패턴 = 상태를 객체로 만든다

상태 패턴이 이걸 푼다.

객체의 상태에 따라 행동이 바뀔 때, 각 상태를 하나의 객체로 만들고 행동을 그 상태 객체에 맡긴다.

문자열 "PAID"를 조건으로 비교하는 대신, PaidState라는 클래스를 만든다. 결제완료 상태에서 무엇을 할 수 있는지는 전부 그 클래스 안에 모인다. 주문은 지금 자기 상태 객체가 시키는 대로 행동한다. 상태를 확인하는 if가 아니라, 상태 객체에 위임하는 다형성으로 바꾸는 것이다.

State 인터페이스로 행동을 정의한다

먼저 상태들이 공통으로 답해야 할 행동을 인터페이스로 정한다.

java
public interface OrderState {
    void ship(Order order);
    void cancel(Order order);
}

각 상태는 이 인터페이스를 구현한다. 자기가 그 행동을 허용하면 처리하고, 안 되면 예외를 던진다.

java
public class PaidState implements OrderState {
    @Override
    public void ship(Order order) {
        // 결제완료 → 배송중으로
        order.setState(new ShippingState());
    }
    @Override
    public void cancel(Order order) {
        // 결제완료 → 환불하며 취소
        order.setState(new CanceledState());
    }
}

public class PendingState implements OrderState {
    @Override
    public void ship(Order order) {
        throw new IllegalStateException("결제 전엔 배송 불가");
    }
    @Override
    public void cancel(Order order) {
        order.setState(new CanceledState());
    }
}

흩어져 있던 규칙이 한자리로 모였다. “결제완료 상태가 무엇을 허용하는가”는 이제 PaidState 하나만 보면 된다.

컨텍스트가 현재 상태에 위임한다

주문(Order)은 상태 패턴에서 **컨텍스트(context)**라 부른다. 컨텍스트는 현재 상태 객체를 참조로 들고, 행동을 그 상태에 위임한다.

java
public class Order {
    private OrderState state = new PendingState();   // 시작 상태

    public void setState(OrderState state) {
        this.state = state;
    }

    public void ship() {
        state.ship(this);      // 현재 상태에 맡긴다
    }

    public void cancel() {
        state.cancel(this);    // 현재 상태에 맡긴다
    }
}

Order.ship()에는 이제 if가 없다. 그냥 state.ship(this)로 넘긴다. 지금 상태가 PaidState면 그쪽 ship()이, PendingState면 그쪽 ship()이 실행된다. 무엇이 실행될지는 현재 상태 객체가 결정한다. 컨텍스트는 자기가 무슨 상태인지 일일이 따지지 않는다.

상태 전이는 상태가 스스로 한다

주목할 점은 다음 상태로 넘어가는 일도 상태 객체가 한다는 것이다. PaidState.ship()을 보면 order.setState(new ShippingState())로 스스로 다음 상태를 지정한다.

diagramdiagram

즉 각 상태는 “내가 이 행동을 받으면 어떤 상태로 넘어가는가”까지 안다. 결제완료는 배송을 받으면 자신을 배송중으로 바꾼다. 전이 규칙이 상태 안에 들어 있어서, 상태 다이어그램의 화살표 하나하나가 코드의 한 지점과 정확히 맞아떨어진다.

조건문이 다형성으로 사라진다

바뀐 것의 핵심은 이것이다. 상태를 비교하던 if가 통째로 사라지고, 다형성이 그 자리를 대신했다.

전에는 “지금 상태가 무엇인지”를 코드가 매번 물었다. 이제는 묻지 않는다. 현재 상태 객체에게 그냥 시키면, 그 객체가 자기에게 맞는 행동을 한다. 상태를 하나 추가할 때도 if를 찾아 헤맬 필요 없이 새 State 클래스 하나만 만들면 된다. 기존 상태 클래스들은 건드리지 않는다.

전략과 무엇이 다른가

상태 패턴은 구조가 전략 패턴과 거의 똑같아 보인다. 인터페이스를 두고 구현을 갈아 끼우는 모양이 닮았다. 하지만 의도가 다르다.

  • 전략은 알고리즘을 밖에서 골라 끼운다. 정렬을 오름차순으로 할지 내림차순으로 할지 클라이언트가 정해 주입한다. 전략들끼리는 서로를 모르고, 스스로 다른 전략으로 바뀌지도 않는다.
  • 상태는 스스로 다음 상태로 전이한다. PaidState는 자기가 언제 ShippingState가 되는지 안다. 상태들은 전이 관계로 서로를 안다.

한마디로 전략은 “밖에서 갈아 끼우는 것”이고, 상태는 “안에서 스스로 넘어가는 것”이다. 교체의 주도권이 클라이언트에 있으면 전략, 상태 객체 자신에게 있으면 상태다.

실무: 상태 머신

상태 패턴은 **상태 머신(state machine)**이 필요한 곳이면 어디에나 어울린다. 주문 처리, 결제 흐름, 문서 결재 워크플로(작성중→검토중→승인됨→반려), 예약 시스템(예약→확정→체크인→완료) 같은 것들이다.

공통점은 “정해진 상태들 사이를 정해진 규칙으로만 오간다”는 것이다. 상태가 서너 개를 넘고 상태별 행동이 제각각이며 전이 규칙이 복잡해지면, 조건문으로 버티는 순간 코드가 무너진다. 이럴 때 각 상태를 객체로 세우면, 상태 하나가 곧 클래스 하나가 되어 다이어그램과 코드가 나란히 간다.

정리

상태 패턴이란상태에 따라 행동이 바뀔 때 상태를 객체로 만든다
문제메서드마다 상태 분기 if가 반복되고 규칙이 흩어진다
고침각 상태를 State 구현 클래스로, 컨텍스트는 현재 상태에 위임
전이다음 상태로 넘어가는 것도 상태 객체가 스스로 한다
효과조건문이 다형성으로 대체되고, 상태 추가는 클래스 추가만으로
전략과전략=밖에서 갈아끼움·서로 모름, 상태=스스로 전이·서로 앎

상태를 조건이 아니라 객체로 세워, 거대한 if를 다형성으로 걷어내는 것 - 그게 상태 패턴이다.

관련 글