상태 패턴(State Pattern)은 행동(Behavioral) 디자인 패턴 중 하나로, 객체가 특정 상태에 따라 다른 행동을 하도록 만드는 패턴이다. 상태 패턴을 사용하면 객체의 내부 상태가 변할 때 객체의 행동이 동적으로 바뀌도록 할 수 있다. 상태 패턴은 객체의 상태를 캡슐화하고, 각 상태에 따른 행동을 상태 객체로 분리하여 관리한다.
“존재의 본질은 고정되지 않고, 상황에 따라 변화하는 행동을 통해 드러난다. 우리의 정체성도 마찬가지다.”
상태 패턴(State Pattern)은 다음과 같은 상황에서 유용하게 사용될 수 있습니다:
- 객체의 행동이 상태에 따라 달라질 때: * 객체의 행동이 그 객체의 상태에 따라 달라져야 할 때 상태 패턴이 유용하다. 상태 패턴을 사용하면 상태별로 행동을 분리하여 관리할 수 있다. 문서 편집기에서 문서가 작성 중, 검토 중, 승인된 상태에 따라 편집, 검토, 승인 기능이 다르게 동작해야 할 때 상태 패턴을 적용할 수 있다.
- 상태 전환이 자주 일어나고, 상태에 따라 객체의 행동이 복잡하게 변화할 때: * 객체가 여러 상태를 가질 수 있으며, 이 상태들 간에 자주 전환이 발생하는 경우 상태 패턴이 유용하다. 상태 패턴은 상태 전환과 관련된 복잡한 조건문을 제거하고, 상태별로 행동을 캡슐화하여 코드의 복잡성을 줄일 수 있다. 게임 캐릭터가 걷기, 뛰기, 점프 등의 여러 상태를 가질 수 있으며, 이 상태들 간에 자주 전환이 일어나는 경우 상태 패턴을 사용해 각 상태를 독립적으로 관리할 수 있다.
- 상태에 따른 코드 중복이 많을 때: * 상태에 따라 코드가 중복되는 경우, 상태 패턴을 사용하여 이러한 중복을 제거하고 상태별로 코드를 관리할 수 있다. 상태별 행동을 각각의 클래스에 캡슐화함으로써 코드의 중복을 줄일 수 있다. 온라인 주문 시스템에서 주문이 접수, 배송 준비 중, 배송 중, 배송 완료 등의 상태를 가질 때, 상태에 따라 동일한 로직이 반복되는 경우 상태 패턴을 사용해 중복 코드를 제거할 수 있다.
- 상태 전환 로직이 복잡하고 관리하기 어려울 때: * 상태 전환 로직이 복잡하여 코드의 유지보수가 어려운 경우, 상태 패턴을 사용해 상태 전환을 명확하게 분리하고 관리할 수 있다. 각 상태는 자체적으로 상태 전환을 관리할 수 있으며, 상태 전환 로직이 분리됨으로써 코드의 가독성과 유지보수성이 향상된다. 복잡한 워크플로우 관리 시스템에서 작업의 상태(예: 대기 중, 처리 중, 완료됨)에 따라 작업이 처리되는 방식이 달라질 때, 상태 패턴을 사용해 상태 전환 로직을 관리할 수 있다.
- 객체가 여러 상태를 가질 수 있고, 상태에 따라 객체의 행동을 독립적으로 관리하고 싶을 때: * 객체가 여러 상태를 가질 수 있으며, 이 상태에 따라 객체의 행동을 독립적으로 관리해야 하는 경우 상태 패턴이 유용하다. 상태별 행동을 각각의 상태 클래스로 캡슐화하여 상태와 행동을 명확히 분리할 수 있다. 은행 계좌가 정상 상태, 초과 인출 상태, 동결 상태 등 다양한 상태를 가질 수 있으며, 상태에 따라 입금 및 출금 동작이 다르게 동작해야 할 때 상태 패턴을 사용해 각 상태를 독립적으로 관리할 수 있다.
장점
- 상태별 행동의 명확한 분리 * 상태 패턴을 사용하면 각 상태별로 행동을 별도의 클래스로 분리하여 관리할 수 있다. 상태 전환 로직과 상태별 행동이 캡슐화되므로, 코드의 가독성과 유지보수성이 향상된다. 은행 계좌에서 정상 상태, 초과 인출 상태, 동결 상태에 따른 행동을 각각의 상태 클래스에 분리하여 구현함으로써, 각 상태의 행동을 독립적으로 관리할 수 있다.
- 복잡한 조건문 및 분기문 제거 * 상태 패턴을 사용하면 상태에 따른 행동을 조건문이나 분기문으로 처리하지 않아도 된다. 상태 전환과 행동을 객체 지향적으로 관리하므로, 복잡한 조건문을 제거하고 코드의 복잡성을 줄일 수 있다. 게임 캐릭터의 상태(걷기, 뛰기, 점프)에 따라 행동을 조건문으로 처리하는 대신, 상태 패턴을 사용해 각 상태별 행동을 상태 객체에 캡슐화하여 관리할 수 있다.
- 유연한 상태 전환 * 상태 패턴을 사용하면 객체의 상태 전환을 유연하게 관리할 수 있습니다. 새로운 상태를 추가하거나 상태 전환 로직을 변경할 때 기존 코드를 수정하지 않고도 쉽게 확장할 수 있다. 온라인 주문 시스템에서 새로운 주문 상태(예: 배송 지연)를 추가할 때, 기존 코드에 영향을 주지 않고 새로운 상태 클래스를 추가하여 시스템을 확장할 수 있다.
- 객체의 상태 변화를 캡슐화 * 상태 패턴은 객체의 상태 변화를 캡슐화하여 외부에서 상태 변화를 직접 조작할 필요 없이 상태 객체를 통해 상태 전환을 관리할 수 있다. 이를 통해 상태 관리의 일관성을 유지할 수 있다. 워크플로우 관리 시스템에서 작업의 상태를 직접 변경하지 않고, 상태 객체를 통해 상태 전환을 관리함으로써 시스템의 일관성을 유지할 수 있다.
- OCP(Open/Closed Principle) 준수 * 상태 패턴은 개방-폐쇄 원칙(OCP)을 준수하여, 기존 상태 클래스에 영향을 주지 않고 새로운 상태를 추가할 수 있다. 상태별 행동을 각각의 클래스에 캡슐화함으로써 코드의 확장성을 높입니다. 결제 시스템에서 새로운 결제 상태(예: 결제 승인 대기)를 추가할 때, 기존 결제 상태 클래스에 영향을 주지 않고 새로운 상태 클래스를 추가할 수 있다.
단점
- 클래스 수 증가 * 상태 패턴을 사용하면 각 상태를 별도의 클래스로 구현해야 하므로, 상태의 수가 많아질수록 클래스의 수도 증가합니다. 이는 코드베이스를 복잡하게 만들고, 관리해야 할 클래스가 많아져 유지보수가 어려워질 수 있다. 복잡한 게임 시스템에서 캐릭터가 여러 상태를 가질 경우, 각 상태별로 클래스를 구현해야 하므로 클래스 수가 급격히 늘어날 수 있다.
- 상태 전환 로직의 분산 * 상태 패턴을 사용하면 상태 전환 로직이 여러 상태 클래스에 분산될 수 있다. 상태 전환 로직이 여러 클래스에 분산되어 있을 경우, 시스템 전체에서 상태 전환이 어떻게 이루어지는지 파악하기 어려워질 수 있다. 워크플로우 시스템에서 상태 전환 로직이 여러 상태 클래스에 흩어져 있다면, 상태 전환 흐름을 파악하기 위해 여러 클래스를 분석해야 할 수 있다.
- 상태 간의 의존성 증가 * 상태 패턴을 사용할 때, 한 상태에서 다른 상태로 전환할 때 상태 간의 의존성이 증가할 수 있다. 이는 상태 클래스 간의 결합도를 높일 수 있으며, 상태 전환 로직이 복잡해질 수 있다. 은행 계좌 시스템에서 계좌 상태 간 전환이 복잡해지면, 상태 클래스 간의 의존성이 증가하여 상태 전환 로직을 수정하기 어려워질 수 있다.
- 모든 상태를 명확하게 정의해야 함 * 상태 패턴을 사용하려면 시스템 내의 모든 상태를 명확하게 정의해야 하며, 상태 전환을 위한 모든 가능성을 고려해야 한다. 이로 인해 설계 초기 단계에서 많은 시간을 투자해야 할 수 있다. 주문 처리 시스템에서 주문의 모든 상태를 명확하게 정의하고, 각 상태 전환을 설계하는 데 많은 시간이 필요할 수 있다.
- 상태 전환이 자주 발생하는 경우 오버헤드 증가 * 상태 전환이 자주 발생하는 시스템에서는 상태 객체의 생성 및 전환에 따른 오버헤드가 발생할 수 있다. 이러한 오버헤드가 시스템의 성능에 영향을 줄 수 있다. 실시간 트랜잭션 처리 시스템에서 상태 전환이 자주 발생하면, 상태 객체의 생성 및 전환에 따른 성능 오버헤드가 발생할 수 있다.
상태 패턴을 사용한 예제:
문서 편집기에서 문서의 상태(초안, 검토 중, 승인됨)에 따라 다른 행동을 수행하는 시스템을 구현
// 상태 인터페이스
interface State {
void edit(Document doc);
void review(Document doc);
void approve(Document doc);
}
// 구체적인 상태 클래스 1: 초안 상태
class DraftState implements State {
@Override
public void edit(Document doc) {
System.out.println("Document is in draft state. You can edit it.");
}
@Override
public void review(Document doc) {
System.out.println("Document is being moved to review state.");
doc.setState(new ReviewState());
}
@Override
public void approve(Document doc) {
System.out.println("Cannot approve the document in draft state.");
}
}
// 구체적인 상태 클래스 2: 검토 중 상태
class ReviewState implements State {
@Override
public void edit(Document doc) {
System.out.println("Cannot edit the document while it's under review.");
}
@Override
public void review(Document doc) {
System.out.println("Document is already under review.");
}
@Override
public void approve(Document doc) {
System.out.println("Document has been approved.");
doc.setState(new ApprovedState());
}
}
// 구체적인 상태 클래스 3: 승인됨 상태
class ApprovedState implements State {
@Override
public void edit(Document doc) {
System.out.println("Cannot edit the document after it has been approved.");
}
@Override
public void review(Document doc) {
System.out.println("Cannot review the document after it has been approved.");
}
@Override
public void approve(Document doc) {
System.out.println("Document is already approved.");
}
}
// Context 클래스: 문서
class Document {
private State state;
public Document() {
state = new DraftState(); // 초기 상태는 초안 상태
}
public void setState(State state) {
this.state = state;
}
public void edit() {
state.edit(this);
}
public void review() {
state.review(this);
}
public void approve() {
state.approve(this);
}
}
// 클라이언트 코드
public class StatePatternExample {
public static void main(String[] args) {
Document document = new Document();
// 초안 상태에서 문서를 편집
document.edit();
// 문서를 검토 중 상태로 변경
document.review();
// 검토 중 상태에서 문서를 승인
document.approve();
// 승인된 상태에서 문서를 편집 시도
document.edit();
}
}

코드 설명
- State 인터페이스 (상태 인터페이스): * State 인터페이스는 문서의 상태에 따라 수행할 수 있는 세 가지 주요 행동, edit(Document doc), review(Document doc), approve(Document doc) 메서드를 정의합니다. 각 상태에서 이 메서드들이 다르게 구현됩니다.
- DraftState, ReviewState, ApprovedState 클래스 (구체적인 상태 클래스): * DraftState 클래스는 문서가 초안 상태일 때의 행동을 정의합니다. 이 상태에서는 문서를 편집할 수 있으며, 검토 중 상태로 전환할 수 있습니다. * ReviewState 클래스는 문서가 검토 중일 때의 행동을 정의합니다. 이 상태에서는 문서를 편집할 수 없으며, 승인 상태로 전환할 수 있습니다. * ApprovedState 클래스는 문서가 승인된 상태일 때의 행동을 정의합니다. 이 상태에서는 문서를 편집하거나 다시 검토할 수 없습니다.
- Document 클래스 (Context 클래스): * Document 클래스는 문서의 상태를 관리하는 컨텍스트 클래스입니다. 이 클래스는 현재 상태 객체를 참조하고 있으며, 편집, 검토, 승인 작업을 상태 객체에게 위임합니다. * setState(State state) 메서드를 통해 문서의 상태를 전환할 수 있습니다.
- 클라이언트 코드: * 클라이언트는 Document 객체를 생성하고, 편집, 검토, 승인 작업을 수행합니다. 문서의 상태에 따라 다른 행동이 수행되며, 상태가 변경될 때 문서는 새로운 상태 객체를 참조하게 됩니다.
관련패턴
- 전략(Strategy) 패턴 * 전략 패턴은 알고리즘을 런타임에 교체할 수 있도록 하는 패턴으로, 상태 패턴과 비슷하게 행동을 캡슐화한다. 두 패턴 모두 행동을 인터페이스로 정의하고, 구체적인 클래스를 통해 구현하며, 클라이언트 코드에서 이를 선택하여 사용할 수 있다. 상태 패턴은 객체의 상태에 따라 행동이 자동으로 전환되는 반면, 전략 패턴은 클라이언트가 행동을 명시적으로 선택하여 설정한다.
- 템플릿 메서드(Template Method) 패턴 * 템플릿 메서드 패턴은 알고리즘의 구조를 정의하고, 알고리즘의 일부 단계를 서브클래스에서 구현하도록 하는 패턴이다. 상태 패턴과 템플릿 메서드 패턴은 알고리즘의 일부를 변경 가능하게 한다는 점에서 유사하다. 상태 패턴은 상태 객체로 행동을 캡슐화하여 상태 전환을 관리하고, 템플릿 메서드 패턴은 알고리즘의 골격을 상위 클래스에서 정의하고, 서브클래스에서 상태에 따라 특정 단계의 동작을 달리할 수 있다.
- 의존성 주입(Dependency Injection) 패턴 * 의존성 주입 패턴은 객체 간의 의존성을 외부에서 주입하여 객체의 생성과 사용을 분리하는 패턴이다. 상태 패턴과 함께 사용될 때, 의존성 주입 패턴을 통해 상태 객체를 주입할 수 있다. 이를 통해 상태 전환을 보다 유연하게 관리하고, 상태 객체의 생성을 클라이언트 코드로부터 분리할 수 있다.
- 플라이웨이트(Flyweight) 패턴 * 플라이웨이트 패턴은 메모리를 절약하기 위해 동일한 객체를 공유하는 패턴이다. 상태 패턴에서 상태 객체가 공유 가능할 때, 플라이웨이트 패턴을 사용하여 상태 객체를 공유할 수 있다. 상태 패턴을 사용할 때, 여러 객체가 동일한 상태를 가지는 경우가 많을 수 있다. 이 경우 플라이웨이트 패턴을 사용하여 상태 객체를 공유하고 메모리 사용을 줄일 수 있다.
- 커맨드(Command) 패턴 * 커맨드 패턴은 요청을 객체로 캡슐화하여, 클라이언트가 요청을 실행할 수 있도록 하는 패턴이다. 상태 패턴과 커맨드 패턴은 모두 행동을 캡슐화하여 클라이언트가 행동의 세부 사항을 알 필요 없이 요청을 실행할 수 있게 한다. 상태 패턴은 객체의 상태에 따라 행동을 관리하고, 커맨드 패턴은 요청을 캡슐화하여 행동을 관리한다.
- 브리지(Bridge) 패턴 * 브리지 패턴은 추상화와 구현을 분리하여, 이들이 독립적으로 변형될 수 있도록 하는 패턴이다. 상태 패턴과 브리지 패턴은 모두 객체의 행동을 분리하여 관리할 수 있다는 점에서 유사하다. 상태 패턴은 상태와 행동을 분리하고, 브리지 패턴은 추상화와 구현을 분리하여 독립적인 변경을 가능하게 한다.
- 옵저버(Observer) 패턴 * 옵저버 패턴은 객체의 상태 변화에 따라 다른 객체들에게 통지하고, 이들이 자동으로 갱신되도록 하는 패턴이다. 상태 패턴과 옵저버 패턴은 상태 변화에 따른 행동을 관리할 수 있다는 점에서 유사하다. 상태 패턴은 객체의 상태에 따라 행동을 변경하고, 옵저버 패턴은 상태 변화에 따라 다른 객체들에게 변화를 통지한다.