FIF's 코딩팩토리

스테이트 패턴(State Pattern) 정리 본문

Back-End/Design Pattern(디자인 패턴)

스테이트 패턴(State Pattern) 정리

FIF 2019. 6. 10. 14:45
반응형

스테이트 패턴(State Pattern) 정의

객체의 내부 상태가 바뀜에 따라 객체의 행동을 바꿀 수 있다.

마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.

 

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

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

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

 

“객체의 클래스가 바뀌는 것과 같은” 이라는 표현을 쓴 이유는?

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

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

 

스테이트 패턴 다이어그램

 

1. Context

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

 

2. State

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

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

 

3. ConcreteState

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

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

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

 

뽑기기계에 대한 예시

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

우리가 뽑기기계에 동전을 넣고 손잡이를 돌리고 어떤 물체가 나오기까지 일련의 과정을 어떤 행동과 상태로 표현할 수 있을까?

상태

(1) 동전 없음

(2) 동전 있음

(3) 알맹이 판매

(4) 알맹이 매진

행동

(1) 동전 투입

(2) 동전 반환

(3) 손잡이 돌림

(4) 알맹이 내보냄

A. 알맹이 매진 상태로 전환

B. 동전없음 상태로 전환

 

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

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

 

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

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

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

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

 

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

 

GumballMachine.java

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 객체에게 행동을 위임하면 된다.

 

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

 

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

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

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

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

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

 

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

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

 

이점

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

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

3. 각 상태를 변경에 대해서는 닫혀 있도록 하면서도 뽑기 기계 자체는 새로운 클래스를 추가하는 확장에 대해서는 열려 있도록 했다. – OCP

 

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

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

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

내부 상태에 따라 현재 상태를 나타내는 객체가 바뀌게 되고 결국 Context 객체의 행동도 자연스럽게 바뀌게 된다.

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

 

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

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

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

 

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

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

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

 

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

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

 

의문점

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

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

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

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

 

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

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

 

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

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

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

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

 

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

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

반응형
Comments