State Pattern 정리

테사벨로그·2025년 10월 23일

Design Pattern

목록 보기
9/19

1. 왜 State Pattern이 생겨났는가?

문제 상황

// ❌ 나쁜 예: 조건문으로 가득한 코드
public class GumballMachine {
    final static int SOLD_OUT = 0;
    final static int NO_QUARTER = 1;
    final static int HAS_QUARTER = 2;
    final static int SOLD = 3;
    int state = SOLD_OUT;
    
    public void insertQuarter() {
        if (state == HAS_QUARTER) {
            System.out.println("You can't insert another quarter");
        } else if (state == SOLD_OUT) {
            System.out.println("You can't insert a quarter");
        } else if (state == SOLD) {
            System.out.println("Please wait, already giving you a gumball");
        } else if (state == NO_QUARTER) {
            state = HAS_QUARTER;
            System.out.println("You inserted a quarter");
        }
    }
    
    // ejectQuarter(), turnCrank(), dispense()도 동일하게 복잡...
}

문제점:

  • 모든 메서드마다 복잡한 조건문 (if-else, switch) 반복
  • 새로운 상태 추가 시 모든 메서드 수정 필요
  • 상태 전환 로직이 여러 곳에 분산
  • 코드 유지보수가 매우 어려움
  • OCP(Open-Closed Principle) 위반

2. State Interface VS Context Class

1. State Interface

  • "각 상태마다 할 수 있는 행동의 규격"
  • 모든 상태가 구현해야 하는 공통 메서드 정의
  • "can-do" 관계 (각 상태가 할 수 있는 행동)
public interface State {
    void insertQuarter();
    void ejectQuarter();
    void turnCrank();
    void dispense();
}

왜 Interface인가?

  • 각 상태마다 완전히 다른 동작 수행
  • NoQuarterState, HasQuarterState, SoldState 등 다양한 상태 가능
  • Context는 State 인터페이스만 알면 됨 (느슨한 결합)

2. Context Class (GumballMachine)

  • "현재 상태를 가지고 있는 객체"
  • 상태 객체들을 관리하고 위임(delegation) 수행
  • "has-a" 관계 (Context는 State를 가짐)
public class GumballMachine {
    State currentState;  // 현재 상태
    
    public void insertQuarter() {
        currentState.insertQuarter();  // 현재 상태에게 위임
    }
}

Context의 역할:

  • 모든 상태 객체 보유
  • 현재 상태에게 행동 위임
  • 상태 전환 허용 (setState)

3. 왜 Abstract Class가 아닌 Interface인가?

Interface를 사용하는 이유

  1. 공통 구현이 없음

    // ✅ 각 상태마다 완전히 다른 구현
    public class NoQuarterState implements State {
        public void insertQuarter() {
            // 동전 삽입 → HasQuarter 상태로 전환
        }
    }
    
    public class HasQuarterState implements State {
        public void insertQuarter() {
            // 이미 동전이 있음 → 거부
        }
    }
  2. 상태별 독립적인 동작

    • 각 ConcreteState는 자신의 상태에서만 의미있는 행동 구현
    • 공유할 코드가 없으므로 Abstract Class 불필요
  3. 느슨한 결합

    • Context는 State 인터페이스만 의존
    • 새로운 상태 추가가 쉬움
    • 기존 코드 수정 없이 확장 가능 (OCP)

4. State Pattern 핵심 구조

      Context (1)  ────────> State (N)
   (상태를 가진 객체)      (상태별 행동들)
   
   - 현재 상태에 따라 행동이 달라짐
   - 상태 전환 시 Context의 동작이 자동으로 변경
   - 실행 중 동적으로 상태 변경 가능

핵심 메커니즘:
1. Context가 요청을 받으면
2. 현재 State 객체에게 위임
3. State 객체가 자신의 행동 수행
4. 필요시 Context의 상태 변경 (setState)


5. 예시 코드

Step 1: State 인터페이스 정의

// 모든 상태가 구현해야 하는 인터페이스
public interface State {
    void insertQuarter();   // 동전 넣기
    void ejectQuarter();    // 동전 반환
    void turnCrank();       // 손잡이 돌리기
    void dispense();        // 검볼 배출
}

Step 2: Context 클래스 구현

public class GumballMachine {
    // 모든 가능한 상태들
    State soldOutState;
    State noQuarterState;
    State hasQuarterState;
    State soldState;
    
    State state;  // 현재 상태
    int count;    // 남은 검볼 개수
    
    public GumballMachine(int numberGumballs) {
        // 모든 상태 객체 생성 (한 번만)
        soldOutState = new SoldOutState(this);
        noQuarterState = new NoQuarterState(this);
        hasQuarterState = new HasQuarterState(this);
        soldState = new SoldState(this);
        
        this.count = numberGumballs;
        if (numberGumballs > 0) {
            state = noQuarterState;  // 초기 상태
        } else {
            state = soldOutState;
        }
    }
    
    // 모든 행동을 현재 상태에게 위임
    public void insertQuarter() {
        state.insertQuarter();
    }
    
    public void ejectQuarter() {
        state.ejectQuarter();
    }
    
    public void turnCrank() {
        state.turnCrank();
        state.dispense();  // 손잡이를 돌리면 자동으로 배출
    }
    
    // 상태 전환 메서드
    void setState(State state) {
        this.state = state;
    }
    
    // 검볼 배출
    void releaseBall() {
        System.out.println("검볼이 나옵니다...");
        if (count > 0) {
            count--;
        }
    }
    
    // Getter 메서드들
    State getNoQuarterState() { return noQuarterState; }
    State getHasQuarterState() { return hasQuarterState; }
    State getSoldState() { return soldState; }
    State getSoldOutState() { return soldOutState; }
    int getCount() { return count; }
}

Step 3: 각 상태 구현

// 1. NoQuarterState - 동전이 없는 상태
public class NoQuarterState implements State {
    GumballMachine gumballMachine;
    
    public NoQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
    
    public void insertQuarter() {
        System.out.println("동전을 넣었습니다");
        // 상태 전환: NoQuarter → HasQuarter
        gumballMachine.setState(gumballMachine.getHasQuarterState());
    }
    
    public void ejectQuarter() {
        System.out.println("동전을 넣지 않았습니다");
    }
    
    public void turnCrank() {
        System.out.println("손잡이를 돌렸지만 동전이 없습니다");
    }
    
    public void dispense() {
        System.out.println("검볼이 배출되지 않습니다");
    }
}

// 2. HasQuarterState - 동전이 있는 상태
public class HasQuarterState implements State {
    GumballMachine gumballMachine;
    
    public HasQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
    
    public void insertQuarter() {
        System.out.println("동전은 한 개만 넣을 수 있습니다");
    }
    
    public void ejectQuarter() {
        System.out.println("동전이 반환됩니다");
        // 상태 전환: HasQuarter → NoQuarter
        gumballMachine.setState(gumballMachine.getNoQuarterState());
    }
    
    public void turnCrank() {
        System.out.println("손잡이를 돌렸습니다");
        // 상태 전환: HasQuarter → Sold
        gumballMachine.setState(gumballMachine.getSoldState());
    }
    
    public void dispense() {
        System.out.println("검볼이 배출되지 않습니다");
    }
}

// 3. SoldState - 검볼 판매 중 상태
public class SoldState implements State {
    GumballMachine gumballMachine;
    
    public SoldState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
    
    public void insertQuarter() {
        System.out.println("잠시만 기다려주세요. 검볼이 나오고 있습니다");
    }
    
    public void ejectQuarter() {
        System.out.println("이미 손잡이를 돌렸습니다");
    }
    
    public void turnCrank() {
        System.out.println("두 번 돌려도 검볼은 하나만 나옵니다");
    }
    
    public void dispense() {
        gumballMachine.releaseBall();
        if (gumballMachine.getCount() > 0) {
            // 상태 전환: Sold → NoQuarter
            gumballMachine.setState(gumballMachine.getNoQuarterState());
        } else {
            System.out.println("검볼이 모두 소진되었습니다!");
            // 상태 전환: Sold → SoldOut
            gumballMachine.setState(gumballMachine.getSoldOutState());
        }
    }
}

// 4. SoldOutState - 품절 상태
public class SoldOutState implements State {
    GumballMachine gumballMachine;
    
    public SoldOutState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
    
    public void insertQuarter() {
        System.out.println("품절입니다. 동전을 넣을 수 없습니다");
    }
    
    public void ejectQuarter() {
        System.out.println("동전을 넣지 않았습니다");
    }
    
    public void turnCrank() {
        System.out.println("품절입니다");
    }
    
    public void dispense() {
        System.out.println("검볼이 배출되지 않습니다");
    }
}

Step 4: 실행

public class GumballMachineTestDrive {
    public static void main(String[] args) {
        GumballMachine gumballMachine = new GumballMachine(5);
        
        System.out.println(gumballMachine);
        
        // 테스트 1: 정상 구매
        gumballMachine.insertQuarter();
        gumballMachine.turnCrank();
        
        System.out.println(gumballMachine);
        
        // 테스트 2: 동전 넣고 반환
        gumballMachine.insertQuarter();
        gumballMachine.ejectQuarter();
        gumballMachine.turnCrank();
        
        System.out.println(gumballMachine);
    }
}

출력 결과

초기 상태: 검볼 5개, 상태=NoQuarter

동전을 넣었습니다
손잡이를 돌렸습니다
검볼이 나옵니다...
남은 검볼: 4개, 상태=NoQuarter

동전을 넣었습니다
동전이 반환됩니다
손잡이를 돌렸지만 동전이 없습니다

6. 실전 예제: Winner State 추가 (10% 확률로 2개 배출)

새로운 Winner State 추가

public class WinnerState implements State {
    GumballMachine gumballMachine;
    
    public WinnerState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
    
    public void insertQuarter() {
        System.out.println("잠시만 기다려주세요");
    }
    
    public void ejectQuarter() {
        System.out.println("이미 손잡이를 돌렸습니다");
    }
    
    public void turnCrank() {
        System.out.println("두 번 돌릴 수 없습니다");
    }
    
    public void dispense() {
        System.out.println("🎉 당첨! 검볼 2개가 나옵니다!");
        gumballMachine.releaseBall();
        if (gumballMachine.getCount() > 0) {
            gumballMachine.releaseBall();
            if (gumballMachine.getCount() > 0) {
                gumballMachine.setState(gumballMachine.getNoQuarterState());
            } else {
                gumballMachine.setState(gumballMachine.getSoldOutState());
            }
        } else {
            gumballMachine.setState(gumballMachine.getSoldOutState());
        }
    }
}

HasQuarterState 수정 (10% 확률 추가)

public class HasQuarterState implements State {
    Random randomWinner = new Random();
    GumballMachine gumballMachine;
    
    public HasQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
    
    public void turnCrank() {
        System.out.println("손잡이를 돌렸습니다");
        int winner = randomWinner.nextInt(10);
        
        // 10% 확률로 Winner 상태로 전환
        if ((winner == 0) && (gumballMachine.getCount() > 1)) {
            gumballMachine.setState(gumballMachine.getWinnerState());
        } else {
            gumballMachine.setState(gumballMachine.getSoldState());
        }
    }
    
    // 나머지 메서드들...
}

새로운 상태 추가의 장점:

  • ✅ 기존 코드 수정 최소화
  • ✅ 새 클래스만 추가하면 됨
  • ✅ 다른 상태들에 영향 없음
  • ✅ OCP(Open-Closed Principle) 준수

7. 핵심 정리

State Pattern의 구성

요소역할특징
State Interface모든 상태의 공통 메서드 정의느슨한 결합 보장
ConcreteState각 상태별 구체적인 행동 구현상태마다 다른 동작
Context상태 객체들을 보유하고 위임현재 상태 관리 및 전환

언제 사용하는가?

  • 객체의 행동이 상태에 따라 달라질 때
  • 복잡한 조건문(if-else, switch)이 많을 때
  • 상태 전환이 명확하고 많을 때
  • 실행 중 동적으로 행동이 변경되어야 할 때

핵심 원칙

  1. 상태별 행동 캡슐화: 각 상태를 별도 클래스로 분리
  2. Context에 위임: Context는 현재 상태에게 행동 위임
  3. 동적 상태 전환: 실행 중 자유롭게 상태 변경
  4. OCP 준수: 새 상태 추가 시 기존 코드 수정 불필요

State vs Strategy 비교

특징State PatternStrategy Pattern
목적상태에 따른 행동 변경알고리즘 교체
변경 시점실행 중 자동 변경클라이언트가 선택
상태 전환Context/State가 관리없음
사용 예검볼 머신, TCP 연결, 문서 상태정렬 알고리즘, 압축 방식

장점

  • 조건문 제거: if-else 지옥에서 탈출
  • 유지보수 용이: 상태별 코드가 한 곳에
  • 확장성: 새로운 상태 추가가 쉬움
  • 명확한 구조: 상태 다이어그램과 1:1 매칭

단점

  • 클래스 증가: 상태마다 클래스 필요
  • Context 의존: State가 Context를 알아야 함

8. 실전 적용 사례

// 예: 문서 승인 워크플로우
public interface DocumentState {
    void submit();   // 제출
    void approve();  // 승인
    void reject();   // 거부
    void publish();  // 발행
}

// Draft → Submitted → Approved → Published
// 각 상태에서 가능한 동작만 허용

State Pattern은 "객체가 상태 기계(State Machine)처럼 동작해야 할 때" 최고의 선택! 🎯

profile
다들 응원합니다.

0개의 댓글