Lee's Grow up

[디자인패턴/Design Pattern] State Pattern / 스태이트 패턴 본문

PROGRAMMING/디자인패턴

[디자인패턴/Design Pattern] State Pattern / 스태이트 패턴

효기로그 2019. 12. 17. 16:56
반응형

관련 내용은 [자바 언어로 배우는 디자인 패턴 입문],[Head First Design Pattern]의 내용을 참고해서 정리한 내용입니다. 잘못된 부분은 댓글로 피드백 부탁드립니다.

1. State 패턴이란?


객체의 내부 상태가 바뀜에 따라서 객체의 행동을 변경할 수 있도록 해주는 패턴

2. State 패턴의 등장인물


  • State(상태)의 역할
    1. 상태가 변할 때마다 다른 동작을 하는 인터페이스(API)를 결정합니다.
  • ConcreteState(구체적인 상태)의 역할
    1. State의 인터페이스(API)를 구체적으로 구현합니다.
  • Context(상황)의 역할
    1. 현재의 상태를 나타내는 ConcreteState 역할을 가집니다. 또한 State 패턴의 이용자에게 필요한 인터페이스(API)를 결정합니다.
      State 패턴의 클래스 다이어그램

3. 예제


어렸을적 동전을 넣고 레버를 돌리면 장난감 캡슐을 주던 기계를 구현한다고 가정해봅니다.
이때, 해당 기계가 가지는 상태를 대충 정리해보면

  1. 동전이 있음
  2. 동전이 없음
  3. 캡슐 출력 중
  4. 캡슐 매진

간단하게 위와 같은 상황이 있다고 가정하고 클래스를 작성해봅니다.

3-1. 예제 GumballMachin 클래스
public class GumballMachin {
    enum State {
        HAS_QUARTER, NO_QUARTER, SOLD_OUT, SOLD
    }

    private State state = State.NO_QUARTER;

    // 동전 넣기
    public void insertQuarter() {
        if(state == State.HAS_QUARTER) {
            System.out.println("이미 동전이 있습니다.");
        } else if ( state == State.NO_QUARTER) {
            state = State.HAS_QUARTER;
            System.out.println("동전을 추가하셨습니다.");
        } else if ( state == State.SOLD_OUT) {
            System.out.println("캡슐이 매진되었습니다.");
        } else if ( state == State.SOLD) {
            System.out.println("캡슐이 나오고 있습니다. 잠시만 기다려주세요.");
        }
    }

    // 동전 반환하기
    public void ejectQuarter() {
        if(state == State.HAS_QUARTER) {
            System.out.println("동전이 반환됩니다.");
            state = State.NO_QUARTER;
        } else if ( state == State.NO_QUARTER) {
            System.out.println("동전을 추가해주세요.");
        } else if ( state == State.SOLD_OUT) {
            System.out.println("동전을 넣지 않으셨습니다.");
        } else if ( state == State.SOLD) {
            System.out.println("이미 캡슐을 뽑으셨습니다.");
        }
    }
    // 손잡이 돌리기
    // 캡슐 꺼내기
}

위와 같이 코드를 작성했다고 했을 경우, 행동이 생략된 손잡이 돌리기, 캡슐 꺼내기 외 여러가지가 더 있는 상황이라고 가정해봅니다.
이때 전혀 새로운 상태가 추가 된다면? 새로운 이프문을 추가하고... 검증하고... 생각만해도 머리가 아픕니다.
이럴경우 사용하는 패턴이 State 패턴입니다.

4. 해결방법


해결방법은 상태라는 인터페이스를 구현하고 각각의 상태에서 행동을 정의하는 방식으로 설계를 합니다.

4-1. 클래스 다이어그램

4-2. GumballMachine 클래스
public class GumballMachin {

    State soldState;
    State soldOutState;
    State hasQuarterState;
    State noQuarterState;

    State state = soldOutState;
    int count = 0;

    public GumballMachin(int count) {
        this.soldState = new SoldState(this);
        this.soldOutState = new SoldOutState(this);
        this.hasQuarterState = new HasQuarterstate(this);
        this.noQuarterState = new NoQuarterState(this);
        this.count = count;
        if(count > 0) {
            state = noQuarterState;
        }
    }

    public void insertQuarter() {
        state.insertQuarter();
    }

    // 기타 필요 메소드
}

위와 같이 객체들을 구성으로 가지며, 생성자를 통해 초기화를 합니다. 이때 본인을 매개변수로 넘겨서 해당 State 클래스에서 참조할 수 있도록 합니다.

4-2. State 인터페이스
public interface State {
    public void insertQuarter();
    public void ejectQuarter();
    public void turnCrank();
    public void dispense();
}

공통된 행동을 정의합니다.

4-3. NoQuarterState 클래스
public class NoQuarterState implements State {
    GumballMachin gumballMachine;

    public NoQuarterState(GumballMachin gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    @Override
    public void dispense() {
        System.out.println("동전을 넣어주세요.");
    }

    @Override
    public void ejectQuarter() {
        System.out.println("동전을 넣어주세요.");
    }

    @Override
    public void insertQuarter() {
        System.out.println("동전을 넣으셨습니다.");

    }

    public void turnCrank() {
        System.out.println("동전을 넣어주세요.");
    }
}

해당 클래스와 같이 각각의 상태에 따라 행동을 정의해줍니다.
다른 HasQuarterstate, SoldOutState, SoldState 도 알맞게 정의해줍니다.

4-4. Main 클래스
public class Main {
    public static void main(String[] args) {

        GumballMachin gumballMachin = new GumballMachin(5);
        // 동전을 넣고 래버 당기기
        gumballMachin.insertQuarter();
        gumballMachin.turnCrank();

        // 동전을 넣고 동전 반환 받기
        gumballMachin.insertQuarter();
        gumballMachin.ejectQuarter();

        // 필요한 로직 작성...
    }
}
왜 사용할까?
  • 클라이언트가 직접 상태를 조작하지 않아도, 객체의 상태를 각 ConcreteState 클래스들이 컨트롤 해주기 때문에 별도의 상태 조작 없이 객체를 사용할 수 있다.

5. 관련 패턴


  • Singleton 패턴 : ConcreteState 역할은 Singleton 패턴으로 구현되는 경우가 있습니다. link
  • Flyweight 패턴 : 상태를 표시하는 클래스는 인스턴스를 갖지 않습니다. 따라서 해당 패턴을 사용해서 ConcreteState 역할을 복수의 Context 역할에서 공유할 수 있는 경우가 있습니다.

6. 비교하기


얼핏 보면 클레스 다이어그램이 Strategy 패턴과 차이가 없어 보입니다. 하지만 이 두 패턴은 용도에 있어서 차이가 있습니다.

  • Strategy 패턴 : 클라이언트가 어떤 전략을 사용할지 지정해줘야 합니다. 즉 사용자가 쉽게 알고리즘 전략을 바꿀 수 있도록 유연성 제공 link
  • State 패턴 : 상태 객체의 일련의 행동이 캡슐화 되고, 그 객체의 내부 상태에 따라 현재 상태를 나타내는 객체가 바뀌게 되고, 자연스럽게 행동도 변경됩니다. 즉 클라이언트는 객체의 상태에 대해서 아무것도 몰라도 됩니다.
반응형
Comments