"캐릭터가 '대기' 중일 때는 점프가 가능하지만, '공격' 중일 때는 점프가 안 됩니다."
"주문이 '배송 중'일 때는 취소가 안 되지만, '준비 중'일 때는 취소가 됩니다."
이런 로직을 짤 때 가장 먼저 떠오르는 것은 if-else 또는 switch 문입니다. 하지만 상태가 늘어날수록 코드는 스파게티가 됩니다. 이를 해결하는 상태 패턴의 두 가지 구현 방법(Classic vs Enum)을 비교해 드립니다.

1. 문제 상황: 거대해지는 조건문
상태 패턴을 쓰지 않으면 모든 메서드에 상태 체크 로직이 들어갑니다.
Java
public void pressJumpButton() {
if (state == IDLE) {
System.out.println("점프!");
} else if (state == ATTACKING) {
System.out.println("공격 중이라 점프 불가"); // 예외 처리
} else if (state == JUMPING) {
System.out.println("2단 점프!");
}
}
이런 코드는 상태가 하나 추가될 때마다 모든 메서드를 찾아다니며 수정해야 하므로 **OCP(개방-폐쇄 원칙)**를 위반합니다.
2. 방식 1: 클래식 상태 패턴 (Class-based) - "정석"
GoF에서 정의한 전통적인 방식입니다. 각각의 상태를 별도의 클래스로 캡슐화합니다.
구조
- State (Interface): 모든 상태가 구현해야 할 행동 정의.
- ConcreteState: 구체적인 상태 클래스들 (IdleState, AttackState 등).
- Context: 현재 상태를 가지고 있는 주체 (Player, Order 등).
코드 예시
Java
// 1. 상태 인터페이스
interface PlayerState {
void jump();
void attack();
}
// 2. 대기 상태 (구현체 1)
class IdleState implements PlayerState {
public void jump() { System.out.println("점프합니다."); }
public void attack() { System.out.println("공격합니다."); }
}
// 3. 공격 상태 (구현체 2)
class AttackState implements PlayerState {
public void jump() { System.out.println("공격 중엔 점프 불가!"); }
public void attack() { System.out.println("연속 공격!"); }
}
// 4. 플레이어 (Context)
class Player {
private PlayerState state;
public void setState(PlayerState state) { this.state = state; }
public void jump() {
state.jump(); // 상태에게 행동 위임 (Delegation)
}
}
장단점
- 장점: 각 상태의 로직이 확실히 분리되어 복잡한 비즈니스 로직을 처리하기 좋습니다. 상태 클래스 내에서 외부 서비스(DB, API)를 주입받아 쓰기도 편합니다.
- 단점: 상태가 많아지면 클래스 파일 개수가 너무 많아집니다. (State Explosion)
3. 방식 2: Enum 상태 패턴 (Enum-based) - "실무 최적화"
자바의 enum은 단순 상수가 아니라, 메서드를 가질 수 있는 특수 클래스입니다. 이를 이용하면 상태 패턴을 파일 하나로 끝낼 수 있습니다.
특징
- 여러 개의 클래스 파일을 만들 필요 없이, Enum 상수 하나하나가 상태 객체 역할을 합니다.
코드 예시
Java
public enum PlayerState {
IDLE {
@Override
public void jump() { System.out.println("점프합니다."); }
@Override
public void attack() { System.out.println("공격합니다."); }
},
ATTACKING {
@Override
public void jump() { System.out.println("공격 중엔 점프 불가!"); }
@Override
public void attack() { System.out.println("연속 공격!"); }
};
// 추상 메서드 정의 (각 상수가 구현하도록 강제)
public abstract void jump();
public abstract void attack();
}
// 사용 (Client)
PlayerState currentState = PlayerState.IDLE;
currentState.jump(); // "점프합니다."
장단점
- 장점:
- 응집도 최강: 관련된 상태 로직이 한눈에 들어옵니다.
- 관리 용이: 파일 하나만 관리하면 되므로 코드가 매우 깔끔해집니다.
- 단점:
- Enum은 Spring Bean이 아니므로, 내부에서 @Autowired로 Service나 Repository를 주입받아 쓰기가 까다롭습니다. (별도로 주입해줘야 함)
4. 비교 및 요약: 언제 무엇을 쓰는가?
| 구분 | Classic State Pattern (Class) | Enum State Pattern (Enum) |
| 구조 | 상태별로 별도 .java 파일 생성 | 하나의 enum 안에 모두 정의 |
| 확장성 | 로직이 길고 복잡할 때 유리 | 로직이 짧고 단순할 때 유리 |
| 의존성 주입 | 스프링 빈 주입 받기 쉬움 | 의존성 주입이 어려움 |
| 추천 상황 | 복잡한 주문 처리, 워크플로우 엔진 | UI 상태, 간단한 게임 로직, 문서 상태 |
결론
상태별 로직이 5~10줄 내외로 간단하고 외부 의존성이 없다면 Enum 상태 패턴이 압도적으로 좋습니다. 코드가 섹시해지거든요.
하지만 상태별로 DB에 접근하거나 복잡한 계산 로직이 필요하다면, 전통적인 클래스 기반 상태 패턴을 사용하여 Spring의 DI(의존성 주입) 혜택을 받는 것이 유지보수에 유리합니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| 책임 연쇄 패턴(Chain of Responsibility) 총정리 (0) | 2025.12.07 |
|---|---|
| 이터레이터 패턴(Iterator Pattern) 완벽 정리 (0) | 2025.12.07 |
| 템플릿 메서드 패턴(Template Method Pattern) 총정리 (0) | 2025.12.07 |
| 빌더 패턴(Builder Pattern) 총정리 (0) | 2025.12.07 |
| 컴포지트 패턴(Composite Pattern) (0) | 2025.12.07 |