지난 포스팅에선 트리 구조를 구성하여 부분과 전체를 나타내는 계층구조로 표현할 수 있는 컴포지트 패턴에 대해 알아보았다. 이번에는 객체 내부의 상태가 바뀜에 따라 객체의 행동을 바꿀 수 있는 스테이트 패턴에 대해 복습한다.

스테이트 패턴은 상태를 별도의 클래스로 캡슐화한 다음 현재 상태를 나타내는 객체에게 행동을 위임한다. 객체의 내부 상태가 바뀜에 따라서 객체의 행동을 변경한다. 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다. 즉, 내부 상태가 바뀜에 따라 행동이 달라지게 된다.

스테이트 패턴은 if, switch문과 같은 분기문을 패턴을 이용해 캡슐화한다. 객체의 클래스가 바뀌는 것과 같은 이라는 표현을 쓴 이유는 무엇일까? 클라이언트 입장에서는 사용하는 객체의 행동이 완전히 달라진다면 마치 그 객체가 다른 클래스로부터 만들어진 객체처럼 느껴진다. 실제로는 다른 클래스로 변신하는 게 아니고 구성을 통해서 여러 상태 객체를 바꿔가면서 사용한다.

다이어그램

Context는 여러 가지 내부 상태를 가질 수 있는 클래스로, request() 메소드가 호출되면 상태 객체에게 그 작업을 위임한다.

State는 모든 구상 상태클래스에 대한 공통 인터페이스를 정의한다.

ConcreateState는 Context로 부터 전달된 요청을 처리하는 구상 상태클래스이다. 구상 클래스는 요청을 처리하는 방법을 자기 나름의 방식으로 구현한다.

예시

뽑기기계에 대한 예시를 살펴보자. 클래스는 상태와 행동으로 모든 것을 표현한다. 뽑기기계에 동전을 넣고 손잡이를 돌리고 어떤 물체가 나오기까지 일련의 과정을 그려보자.

상태

  • 동전 없음
  • 동전 있음
  • 알맹이 판매
  • 알맹이 매진

행동

  • 동전 투입
    • 동전 있음 상태로 전환
  • 동전 반환
    • 동전 없음 상태로 전환
  • 손잡이 돌림
    • 알맹이 판매 상태로 전환
  • 알맹이를 내보냄
    • 알맹이 매진 상태로 전환
    • 동전없음 상태로 전환

뽑기 기계는 4개의 상태와 4개의 행동으로 구성된다. 행동은 4개지만 다른 상태로 넘어가는 전환의 종류는 5가지다.

  1. 뽑기 기계를 **동전 없음 ** 상태로 시작한다.
  2. 여기서 동전을 넣으면 동전 있음 상태가 된다.
  3. 동전을 넣고 손잡이를 돌리면 알맹이 판매 상태가 된다.
  4. 알맹이 판매 상태는 다시 알맹이의 개수를 검사해서 동전 없음 혹은 알맹이 매진 상태가 된다.

중요한 것은 이런 뽑기 기계를 이용할 때 동전이 없는 상태에서 동전을 반환 받으려고 한다거나, 동전이 이미 들어있는데 하나 더 집어넣으려고 하는 것처럼 이상한 행동을 할 수도 있다는 것을 기억해야 하는 것이다.

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;
    int count = 0;

    public GumballMachine(int count) {
        this.count = count;
        if(count > 0) {
            state = NO_QUARTER;
        }
    }
}

만약, 스테이트 패턴을 이용하지 않고 각각의 행동을 구현하려면 if문으로 state의 상태에 따라 행동을 분기해야 하므로 상태가 추가될 때마다 모든 메소드에 코드를 추가해야 하는 불편함이 있다. 코드를 변경해야 하며 특정 메소드는 더 많이 변경해야 할 수도 있다.

if(상태 == 동전있음) {
	// ...
} else if(상태 == 동전없음) {
	// ...
} else if(상태 == 알맹이 판매) {
	// ...
} else if(상태 == 알맹이 매진) {
	// ...
}

제어문을 통해서 상태에 따라 분기를 할 경우 해당 제어문에 조건과 행동을 계속해서 추가해야한다. 변경이 일어나는 부분은 변하지 않는 부분으로부터 분리해야한다. 스테이트 패턴을 적용해서 각 상태의 행동을 별도의 클래스에 집어넣고 모든 상태에서 각각 자기가 할 일을 구현한다. 이는 구성을 활용하는 것이며 결국 스테이트 패턴을 이용하는 것이다.

스테이트 패턴을 적용하더라도 새로운 상태가 추가되면 코드를 고치긴 해야한다. 하지만, 상태 클래스를 새로 추가하고 전환과 관련된 코드만 손보면 된다. 제어문을 이용한 방식에 비해 훨씬 유연해진다. 이제 뽑기 기계를 스테이트 패턴을 이용해서 변경해보자.

상태를 클래스로 정의하고 행동을 메소드로 정의한다.

public class GumballMachine {
    State soldOutState;
    State noQuarterState;
    State hasQuarterState;
    State soldState;
    State state = soldOutState;

    int count = 0;

    public GumballMachine(int numberGumballs) {
        soldOutState = new SoldOutState(this);
        noQuarterState = new NoQuarter(this);
        hasQuarterState = new HasQuarterState(this);
        soldState = new SoldState(this);
        this.count = numberGumballs;
        if(numberGumballs > 0) {
            state = noQuarterState;
        }
    }
    
    public void insertQuarter() {
        state.insertQuarter();
    }
    
    public void ejectQuarter() {
        state.ejectQuarter();
    }
    
    public void turnQuarter() {
        state.turnQuarter();
    
        
    public void setState(State state) {
        this.state = state;
    }
        
    public void releaseBall() {
        if(count != 0) {
            count -= 1;
        }
    }
}

나머지 코드는 굳이 볼 필요가 없으므로 뽑기 기계의 코드만 살펴보자. 제어문을 통해 상태를 검사해 행동을 분기하는 코드가 없어졌다. 대신 상태를 위한 인터페이스와 구상 클래스를 변수로 갖는다. 각각의 행동이 호출되면 state 객체에게 행동을 위임하면 된다. 결국 뽑기기계의 상태에 맞는 상태 객체가 요청을 위임받아 처리하게 될 것이다.

HasQuarterState가 요청을 위임받았다고 가정해보자. HasQuarterState는 동전이 들어있는 상태를 나타내는 클래스이므로 동전을 넣을 경우 반환하고 오류를 알려야한다. 동전을 반환하는 경우에는 아무 문제가 없으므로 동전을 반환하고 **동전없음 **상태로 전환한다. 반대로 손잡이를 돌릴 경우에는 아무런 문제가 없으므로 알맹이 판태 상태로 전환한다. HasQuarterState처럼 나머지 상태에서도 각각 상태에 알맞게 메소드를 구현하면 된다.

스테이트 패턴을 도입하면서 생긴 이점

  • 각 상태의 행동을 별개의 클래스로 국지화시켰다.
    • 캡슐화
  • 관리하기 힘든 골칫덩어리 if 선언문들을 없앴다.
    • 유연성
  • 각 상태를 변경에 대해서는 닫혀 있도록 하면서도 뽑기 기계 자체는 새로운 클래스를 추가하는 확장에 대해서 열려 있도록 고쳤다.
    • OCP

스테이트 패턴 VS 스트래티지 패턴

스테이트 패턴은 상태 객체에 일련의 행동이 캡슐화된다.

스테이트 패턴에서는 상황에 따라 Context 객체에서 여러 상태 객체 중 한 객체에게 모든 행동을 맡기게 된다. 내부 상태에 따라 현재 상태를 나타내는 객체가 바뀌게 되고, 결국 컨텍스트 객체의 행동도 자연스럽게 바뀌게 된다. 클라이언트는 이러한 상태와 전환 과정을 거의 몰라도 된다.

스트래티지 패턴은 일반적으로 클라이언트에서 컨텍스트 객체한테 어떤 전략 객체를 사용할지를 지정해준다.

스트래티지 패턴은 주로 실행시에 전략 객체를 변경할 수 있는 유연성을 제공한다. 반대로 상황에 따라 적합한 전략 객체를 선택해서 사용한다.

스트래티지 패턴은 서브클래스를 만드는 방법을 대신하여 유연성을 극대화하기 위한 용도로 쓰인다.

상속을 이용해서 클래스의 행동을 정의하다 보면 행동을 변경하기가 힘들다. 구성을 이용하면 행동을 정의하는 객체를 유연하게 바꿀 수 있다.

스테이트 패턴은 컨텍스트 객체에 수많은 조건문을 집어넣는 대신에 사용할 수 있는 패턴이라고 할 수 있다.

행동을 상태 객체 내에 캡슐화시키면 컨텍스트 내의 상태 객체를 바꾸는 것만으로도 컨텍스트 객체의 행동을 바꿀 수 있다.

의문점

뽑기기계의 상태에 의해서 다음 상태가 결정된다. 반드시 구상 상태 클래스에서 다음 상태를 결정해야 하는가?

항상 그렇지는 않다. Context에서 상태 전환의 흐름을 결정하도록 할 수 있다. 상태 전환이 고정되어있는 경우에는 이러한 흐름을 Context에 넣어도 된다. 하지만 상태 전환이 동적으로 결정되는 경우 상태 클래스 내에서 처리하는 것이 좋다.

클라이언트에서 상태 객체하고 직접 연락을 하는 경우도 있는가?

그런 일은 없다. 상태는 Context쪽에서 사용할 내부 상태 및 행동을 표현하기 위한 용도이기 때문에 상태에 대한 요청은 전부 Context로부터 오게 된다.

Context의 인스턴스가 아주 많은 경우 여러 인스턴스에서 상태 객체를 공유할 수 있는가?

실제로 여러 인스턴스에서 상태 객체를 공유하는 경우가 흔히 있다. 상태 객체 내에 자체 상태를 보관하지 않아야 한다는 조건만 만족되면 상관 없다. 상태 객체 내에 자체 상태를 보관해야 한다면, 각 Context마다 유일한 객체가 필요하기 때문이다.

마무리

디자인 패턴을 사용하다보면 필요한 클래스의 개수가 많아지는 것은 어쩔 수 없다. 상황에 맞게 적절한 패턴과 원칙을 적용하고, 필요하다면 변종을 만드는 것도 필요하다.

스테이트 패턴 예제