본문 바로가기

OOP, FP/디자인패턴

헤드퍼스트 디자인 패턴: 12. 스테이트 패턴

지난 포스팅에선 트리 구조를 구성하여 부분과 전체를 나타내는 계층구조로 표현할 수 있는 컴포지트 패턴에 대해 알아보았다.


이번에는 객체 내부의 상태가 바뀜에 따라 객체의 행동을 바꿀 수 있는 스테이트 패턴에 대해 복습한다.


스테이트 패턴

객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있다. 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.


스테이트 패턴은 상태를 별도의 클래스로 캡슐화한 다음 현재 상태를 나타내는 객체에게 행동을 위임한다.

따라서 내부 상태가 바뀌면 행동이 달라지게 된다.

if, switch문과 같은 분기문을 패턴을 이용해 캡슐화, 분리한다고 생각하면 될 것 같다.


객체의 클래스가 바뀌는 것과 같은  이라는 표현을 쓴 이유는 무엇일까?

클라이언트 입장에서는 사용하는 객체의 행동이 완전히 달라진다면 마치 그 객체가 다른 클래스로부터 만들어진 객체처럼 느껴진다.

실제로는 다른 클래스로 변신하는 게 아니고 구성을 통해서 여러 상태 객체를 바꿔가면서 사용한다.




스테이트 패턴의 다이어그램이다.

스트래티지, 싱글턴 등의 그것과 같이 간단한 구조를 가진다.


(1) Context

- 여러 가지 내부 상태를 가질 수 있는 클래스

  request()가 호출되면 상태 객체에거 그 작업을 위임한다.


(2) State

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

  모든 상태 클래스에서 이를 구현하기 때문에 바꿔가면서 사용할 수 있다.


(3) ConcreateState

- Context로 부터 전달된 요청을 처리하는 구상 상태클래스.

  각각의 구상 클래스들은 요청을 처리하는 방법을 자기 나름의 방식으로 구현한다.

  Context에서 상태를 바꾸기만 하면 행동도 같이 바뀌게 된다.


이번에도 책에 나온 예시를 다시 살펴보자.

어렸을 때 자주 하던 뽑기기계에 대해서 예시가 나왔다.


클래스는 상태와 행동으로 모든 것을 표현한다.

그럼 우리가 뽑기기계에 동전을 넣고 손잡이를 돌리고 어떤 물체가 나오기까지 일련의 과정을

어떤 행동과 상태로 표현할 수 있을까?



상태

(1) 동전 없음

(2) 동전 있음

(3) 알맹이 판매

(4) 알맹이 매진


행동

(1) 동전 투입

(2) 동전 반환

(3) 손잡이 돌림

(4) 알맹이 내보냄

  - (4-1) 알맹이 매진 상태로 전환

  - (4-2) 동전없음 상태로 전환


크게 4개의 상태(원 모양)와 4개의 행동으로 이루어질 수 있다.

행동은 4개지만 다른 상태로 넘어가는 전환 종류(화살표 모양)가 5가지다.


뽑기 기계를 처음 시작하면 동전 없음 상태 이다.

여기서 동전을 넣으면 동전 있음 상태가 된다.

동전을 넣고 손잡이를 돌리면 알맹이 판매 상태가 된다.

알맹이 판매 상태는 다시 알맹이의 개수를 검사해서 동전 없음 혹은 알맹이 매진 상태가 된다.


중요한 것은 이런 뽑기 기계를 이용할 때 동전이 없는 상태에서 동전을 반환 받으려고 한다거나

동전이 이미 들어있는데 하나 더 집어넣으려고 하는 것처럼 이상한 행동을 할 수도 있다는 것을 기억해야 하는 것이다.


뽑기 기계 클래스부터 코드를 살펴보자.

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(상태 == 알맹이 매진) {


}


바로 이 부분이 계속해서 바뀌는 부분이 될 것이다.

이를 분리하려면 각 상태의 행동을 별도의 클래스에 집어넣고 모든 상태에서 각각 자기가 할 일을 구현하게 하면 된다.

즉, 구성을 활용하는 것이며 결국 스테이트 패턴을 이용하는 것이다.


여전히 코드를 고치긴 하지만 새롱누 상태를 추가할 때 결국 클래스를 새로 추가하고

몇 군데에서 상태 전환하고 관련된 코드만 조금 손보면 될테니 전에 비해 훨씬 유연하다고 할 수 있다.


자 이제 뽑기 기계를 스테이트 패턴을 이용해서 변경해보자.




4개의 상태와는 각각 클래스로, 4개의 행동은 각각 메소드로 정의한다.

앞에 뽑기기계의 각 상태를 직접 클래스에 대응시키면 된다.

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 NoQuarterthis);

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();

}


void setState(State state) {

this.state = state;

}


void releaseBall() {

if(count != 0) {

count -= 1;

}

}

}


나머지 코드는 굳이 볼 필요가 없으므로 뽑기 기계의 코드만 가져왔다.

제어문을 통해 상태를 검사해 행동을 분기하는 부분이 없어졌다.

그저 상태를 위한 인터페이스와 구상 클래스만 만들고 이를 구성으로 집어넣는다.

각각의 행동이 호출되면 state 객체에게 행동을 위임하면 된다.


그럼 현재 뽑기기계의 상태에 맞는 상태 객체가 요청을 위임받아 처리하게 될 것이다.


예를 들어 HasQuarterState가 요청을 위임받았다고 가정해보자.

동전이 들어있는 상태를 나타내는 클래스이므로 동전을 넣을 경우 넣은 동전을 반환하고 오류를 알려야한다.

동전을 반환하는 경우에는 아무 문제가 없으므로 동전을 반환하고 동전없음 상태로 전환한다.

손잡이를 돌릴 경우 아무 문제가 없으므로 알맹이 판태 상태로 전환한다.

dispance()메소드는 알맹이를 내보내는 메소드인데 SoldState에서만 사용 가능하므로 오류를 알려야한다.


위의 동전있음 상태의 클래스처럼 나머지 상태에서도 각각 상태에 알맞게 메소드를 구현하면 된다.

스테이트 패턴을 이용함으로써 아래와 같은 이점을 얻었다.


이점

(1) 각 상태의 행동을 별개의 클래스로 국지화시켰다. - 캡슐화

(2) 관리하기 힘든 골칫덩어리 if 선언문들을 없앴다. - 유연성

(3) 각 상태를 변경에 대해서는 닫혀 있도록 하면서도 뽑기 기계 자체는 새로운 클래스를

    추가하는 확장에 대해서 열려 있도록 고쳤다. - OCP 


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

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

- 상황에 따라 Context 객체에서 여러 상태 객체 중 한 객체에게 모든 행동을 맡기게 된다.

  내부 상태에 따라 현재 상태를 나타내는 객체가 바뀌게 되고

  결국 컨텍스트 객체의 행동도 자연스럽게 바뀌게 된다.

  클라이언트는 이러한 상태와 전환 과정을 거의 몰라도 된다.


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

- 주로 실행시에 전략 객체를 변경할 수 있는 유연성을 제공한다.

  보통 가장 적합한 전략 객체를 선택해서 사용하게 된다.


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

- 상속을 이용해서 클래스의 행동을 정의하다 보면 행동을 변경해야 할 때 마음 변경하기가 힘들다.

  결국 구성을 통해 행동을 정의하는 객체를 유연하게 바꿀 수 있다.


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

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


의문점

(1) 뽑기기계의 상태에 의해서 다음 상태가 결정되는, 반드시 구상 상태 클래스에서 다음 상태를 결정해야 하는가?

- 항상 그렇지는 않다. Context에서 상태 전환의 흐름을 결정하도록 할 수 있다.

  상태 전환이 고정되어있는 경우에는 이러한 흐름을 Context에 넣어도 된다.

  하지만 상태 전환이 동적으로 결정되는 경우 상태 클래스 내에서 처리하는 것이 좋다.


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

- 그런 일은 없다. 상태는 Context쪽에서 내부 상태 및 행동을 표현하기 위한 용도이기 때문에

  상태에 대한 요청은 전부 Context로부터 오게 된다.


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

- 실제로 그렇게 하는 경우가 흔히 있다.

  상태 객체 내에 자체 상태를 보관하지 않아야 한다는 조건만 만족되면 상관 없다.

  상태 객체 내에 자체 상태를 보관해야 한다면, 각 Context마다 유일한 객체가 필요하기 때문이다.


* 디자인 패턴을 사용하다보면 필요한 클래스의 개수가 많아지는 것은 어쩔 수 없다.

  상황에 맞게 적절한 패턴과 원칙을 적용하고, 필요하다면 변종을 만드는 것도 필요하다.



객체 내부의 상태가 바뀜에 따라 객체의 행동을 바꿀 수 있는 스테이트 패턴에 대해 복습을 마치겠다.


1. 스트래티지 패턴

2. 옵저버 패턴

3. 데코레이터 패턴

4-1. 팩토리 메서드 패턴

4-2. 추상 팩토리 패턴

5. 싱글턴 패턴

6. 커맨드 패턴

7. 어댑터 패턴

8. 퍼사드 패턴

9. 템플릿 메소드 패턴

10. 이터레이터 패턴

11. 컴포지트 패턴

12. 스테이트 패턴