Notice
Recent Posts
Recent Comments
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

State Pattern 본문

Design Pattern/Behavioral Pattern

State Pattern

defacto standard 2017. 9. 26. 10:28

1. 가정

 - 형광등을 구현한다.

 - 버튼은 on, off 2개 뿐이다.

 - 꺼진 상태에서 off를 누르면 반응이 없고, on을 누르면 켜진다.

 - 켜진 상태에서 on을 계속 누르면 색상이 변화한다.

       - on을 누를 때 마다 흰색 - 초록 - 노랑 - 빨강의 색상으로 바뀐다.

 - 빨간색에서 on을 누르면 과부화되어 자동으로 꺼진다.


2. Naive Code


- NaiveLight

public class NaiveLight {
// 현재 전원 상태
// 0 - OFF / 1 - ON / 2 - 초록 / 3 - 노랑 / 4 - 빨강
private int state = 0;

public void off(){

switch (state) {
case 0:
System.out.println("반응 없음");
break;
case 1:
System.out.println("Light OFF");
break;
case 2:
System.out.println("초록색에서 OFF");
break;
case 3:
System.out.println("노랑색에서 OFF");
break;
case 4:
System.out.println("빨간색에서 OFF");
break;
}

state = 0;

}

public void on(){

if(state == 0) {
System.out.println("Light On");
state = 1;
}

else if(state == 1) {
System.out.println("경고 : 초록");
state = 2;
}

else if(state == 2){
System.out.println("경고 : 노랑");
state = 3;
}

else if(state == 3){
System.out.println("경고 : 빨강");
state = 4;
}

else if(state == 4){
System.out.println("과부하로 인해 꺼짐");
state = 0;
}
}

}

형광등 클래스이다. state라는 변수를 두어 현재 값에 따라 어떠한 연산을 한 후 state를 다른 값으로 변환한다. 이후에는 변경된 state값을 기준으로 동작하게 되는 코드이다.


 - NaiveClient

public class NaiveClient {
public static void main(String args[]) {
NaiveLight light = new NaiveLight();

light.off();

light.on();
light.on();
light.on();
light.on();
light.on();
light.off();

light.on();
light.on();
light.on();
light.off();

}
}


NaiveLight를 사용하는 NaiveClient이다. 


실행결과는 다음과 같다.



3. 문제점



NaiveLight 와 같이 if - else if - else 구문 / switch문을 쓰는 경우 상황에 따른 분기가 가능하다.


조건문에 state등과 같은 변수를 선언하고, 상황에 맞는 연산을 수행한 후 state값을 변경하여 다음번에 수행할 로직을 변경할 수 있다.


하지만, 이러한 소스형태는 원천적으로 고칠 수 없는 문제를 가지고있다.


바로 '상황' 또는 '상태'라는 경우의 수가 많아지는 경우이다.


예를 들어, 현재 NaiveLight의 경우

'꺼진 상태'가 state값이 0인 것으로 판단하여 이 상황에 맞게 동작하는 로직이 구현되어 있다.

'켜진 상태'는 state값이 1인 것으로 판단하여 이 상황에 맞게 동작하는 로직이 구현되어 있다.


그렇다면, '상태'라는 경우의 수가 늘어난다면 이에 대한 if-else if-else문을 추가해야 한다는 말이다.


당연히 on, off와 같은 버튼의 기능이 늘어날 때마다 그 내부에 분기문을 추가해야하며, state의 값이 현재는 0, 1 뿐이지만 2 3 4 등 '상태'가 추가된다면 더욱 복잡해진다.


게다가, 기존의 소스코드를 수정해야하고, 이는 기능이 많을수록, 상태가 많을수록 심각해질 것이다.


이렇게 말했다고 해서, 절대 if-else if-else문을 사용하지 말라는것이 아니다. 상황에 맞게 사용해야 한다는 것이다.


사실 이정도 (if문의 깊이가 1인) 소스코드는 짧은 경우에 한정하여 노가다로 해볼만 하다. 생각나는대로 구현할 수 있어 간단한 어플리케이션에서는 이러한 구조가 더 생산성이 나을 수 있다.


하지만 규모가 큰 프로젝트에서 이러한 상태가 여러 개가 존재한다면, if-else if-else 구조만 1천라인이 된다면, 헬게이트가 열리게 된다.


당장 1천라인이 아니라 30줄만되도 짜증날 것이다. 뭐하나 보려면 계속 스크롤 올렸다 내렸다 해야한다.


그리고, if-else if-else 상호간에 중복된 소스코드가 존재한다면 최악이다.


더 큰 문제는, '두 번째 상태'라는 개념이 존재할 때이다. 보통 다중 if문으로 구현되는 경우가 많은 경우를 생각하면 된다.

예를 들어서


if(state1 == 1) {

if(state2 == 1) {
/* do A */
}

else if(state2 == 2) {
/* do B */
}

else {
/* do C */
}
}

else if(state1 == 2) {

if(state2 == 1) {
/* do B */
}

else if(state2 == 2) {
/* do C */
}

else {
/* do A */
}
}

else {

if(state2 == 1) {
/* do C */
}

else if(state2 == 2) {
/* do A */
}

else {
/* do B */
}
}


또는


동작이 A, B, C 3개 밖에 없으니

if(     ( state1 == 1 && state2 == 1 )
|| (state1 == 2 && ( state2 != 1 && state2 != 2 ) )
|| (state1 != 1 && ( state2 != 1 && state2 != 2 ) ) ) {
// do A
}

else if(
( state1 == 1 && state2 == 2 )
|| (state1 == 2 && state2 == 1 )
|| ( (state1 != 1 && state1 != 2) && (state2 != 1 && state2 != 2) ) ) {
// do B
}

else if(
( state1 == 1 && ( state2 != 1 && state2 != 2 ) )
|| ( state1 == 2 && state2 == 2 )
|| ( state1 != 1 && state2 == 1 ) ) {
// do C
}

위와 같은 미친 소스코드를 뽑아낼 여지가 있다.


위 소스코드는 암걸리기에 충분하다.

만약 A, B C 를 구동시킬 때의 조건인 state3이라는 요소가 추가된다면, 암세포가 암걸리기에 충분하다.


'상태'가 추가되거나, 이 상태에 대한 '행동'이 추가 되거나, '상태'에 따른 '행동'의 조건이 변경되는 경우


유지보수의 난이도는 기하급수적으로 상승한다.


이러한 식으로 복잡한 상태가 계속해서 이어지는 경우인데도 불구하고, if-else if-else 구문만을 고집하는건 다음과 같은 경우이다.


1. 적절하지 않은 디자인이라는 예시를 보여주기 위해서

2. 유지보수할 일이 없을 때

3. 협업을 하지 않아 다른 사람이 볼일이 없는 경우

4. 개발실력이 부족할 때


4. 해결방안

현재 문제가 되는 것을 생각해본다.

if-else if-else 문 / switch을 사용하는 이유부터 생각해보자.


바로 '상태'에 따른 분기이다.

'현재가 어떤 상태인지에 따라' 다르게 동작하기 위함이다.


그렇다면 이 '현재상태'라는 것을 클래스화 시켜야한다.


현재 상태는 OFF / ON / 초록 / 노랑 / 빨강 과 같이 존재한다.


즉, 현재 상태에 대한 on off를 실행했을 때에 대한 경우의 수는 다음과 같다.

현재 OFF상태(state==0)라면 on 또는 off를 눌렀을 때 무엇을 해야하는가?

현재 ON상태(state==1)라면 on 또는 off를 눌렀을 때 무엇을 해야하는가?

현재 초록상태(state==2)라면 on 또는 off를 눌렀을 때 무엇을 해야하는가?

현재 노랑상태(state==3)라면 on 또는 off를 눌렀을 때 무엇을 해야하는가?

현재 빨간상태(state==4)라면 on 또는 off를 눌렀을 때 무엇을 해야하는가?


'현재상태' 라는 것은 추상적이다.

따라서 추상적인 개념으로 State를 선언할 것이다.

구체적인 상태인 ConcreteState1~5는 위 5개 상태를 의미하게 될 것이다.


5. Solution Code

 - State

// 현재상태를 추상화
public interface State {

public void on(Context context);
public void off(Context context);

}

현재 상태를 추상화한 State 인터페이스이다.

현재 상태에 따라 on() 또는 off()의 내용이 달라지고, Client 단에서는 추상화된 객체를 통해 구체적인 상태에 관계없이 현재상태에 해당하는 로직을 수행한다.


 - ConcreteState1~5

// OFF State
public class ConcreteState1 implements State {

// Singleton Pattern
private static ConcreteState1 concreteState1 = new ConcreteState1(); // OFF 클래스의 인스턴스로 초기화 됨

private ConcreteState1() {
}

public static ConcreteState1 getInstance() { // 초기화된 OFF 클래스의 인스턴스를 반환함
return concreteState1;
}

public void on(Context context) {
context.setState(ConcreteState2.getInstance());
System.out.println("Light ON");
}

public void off(Context context) {
System.out.println("반응 없음(이미 꺼짐)");
}
}

// ON State
public class ConcreteState2 implements State {

// Singleton Pattern
private static ConcreteState2 concreteState2 = new ConcreteState2(); // ON 클래스의 인스턴스로 초기화 됨\

private ConcreteState2() {
}

public static ConcreteState2 getInstance() { // 초기화된 ON 클래스의 인스턴스를 반환함
return concreteState2;
}

public void on(Context context) {
context.setState(ConcreteState3.getInstance());
System.out.println("경고 : 초록");
}

public void off(Context context) {
context.setState(ConcreteState1.getInstance());
System.out.println("Light OFF");
}
}

// Green State
public class ConcreteState3 implements State {

// Singleton Pattern
private static ConcreteState3 concreteState3 = new ConcreteState3();

private ConcreteState3() {
}

public static ConcreteState3 getInstance() {
return concreteState3;
}

public void on(Context context) {
context.setState(ConcreteState4.getInstance());
System.out.println("경고 : 노랑");
}

public void off(Context context) {
context.setState(ConcreteState1.getInstance());
System.out.println("초록에서 꺼짐");
}
}

// Yellow State
public class ConcreteState4 implements State {

// Singleton Pattern
private static ConcreteState4 concreteState4 = new ConcreteState4();

private ConcreteState4() {
}

public static ConcreteState4 getInstance() {
return concreteState4;
}

public void on(Context context) {
context.setState(ConcreteState5.getInstance());
System.out.println("경고 : 빨강");
}

public void off(Context context) {
context.setState(ConcreteState1.getInstance());
System.out.println("노랑에서 꺼짐");
}
}

// Red State
public class ConcreteState5 implements State {

// Singleton Pattern
private static ConcreteState5 concreteState5 = new ConcreteState5();

private ConcreteState5() {
}

public static ConcreteState5 getInstance() {
return concreteState5;
}

public void on(Context context) {
context.setState(ConcreteState1.getInstance());
System.out.println("과부하로 인해 꺼짐");
}

public void off(Context context) {
context.setState(ConcreteState1.getInstance());
System.out.println("빨강에서 꺼짐");
}
}

'현재 상태'를 의미하는 ConcreteState이다.

on()에는 현재 상태를 기준으로 on 버튼을 눌렀을 때 어떤 식으로 동작하게 되는 지를 작성한다.

off()에는 현재 상태를 기준으로 off 버튼을 눌렀을 때 어떤 식으로 동작하게 되는 지를 작성한다.


Singleton Pattern을 적용한 예시이므로, 이에 대한 내용은 따로 공부하길 바란다.


여기서 집중해야할 점은

1. 현재 상태를 Class화 함

2. 따라서 각 구체적인 현재 상태에 대해서 on() off() 메서드에 기술함


이렇게 2가지이다.


 - Context

// Light
public class Context {
private State state;

public Context() {
// 초기 값
state = ConcreteState1.getInstance();
}

public void setState(State state) {
this.state = state;
}

// Delegation
public void on() {
state.on(this);
}

// Delegation
public void off() {
state.off(this);
}
}

Context클래스는 State에 따른 로직을 수행하는 개체이다. 여기서는 형광등(Light)이 해당된다.

State Pattern에서는 Context(Light)가 어떤 행위(Context# on(), off())를 수행할 때,

State의 행위(State# on(), off())를 수행하도록 함으로써 위임(Delegation)한다.


참고로, Context 클래스와 NaiveLight는 같은 기능을 수행한다. 가독성의 차이가 엄청난 것을 알 수 있다.


 - Client

public class Client {
public static void main(String[] args) {
Context light = new Context();
light.off();

light.on();
light.on();
light.on();
light.on();
light.on();
light.off();

light.on();
light.on();
light.on();
light.off();
}
}

NaiveClient와 동일한 로직으로 수행한다.


수행 결과는 다음과 같다.



6. State Pattern


- UML & Sequence Diagram



 - State : 시스템의 모든 상태에 공통의 인터페이스를 제공한다. 따라서 이 인터페이스를 실체화한 어떤 상태 클래스도 기존 상태 클래스를 대신해 교체해서 사용할 수 있다.

 

 - State 1~3 : Context 객체가 요청한 작업을 자신의 방식으로 실제 실행한다. 대부분의 경우 다음 상태를 결정해 상태 변경을 Context 객체에 요청하는 역할도 수행한다.

 

 - Context : State를 이용하는 역할을 수행한다. 현재 시스템의 상태를 나타내는 상태 변수(state)와 실제 시스템의 상태를 구성하는 여러 가지 변수가 있다. 또한 각 상태 클래스에서 상태 변경을 요청해 상태를 바꿀 수 있도록 하는 메서드(setState)가 제공된다. Context 요소를 구현한 클래스의 request 메서드는 실제 행위를 실행하는 대신 해당 상태 객체에 행위 실행을 위임한다.



조건식으로 분기하는 코드는 이해하거나 수정하기 어렵다.

각 상태를 클래스로 분리하여 각 상태에 해당하는 행동을 클래스의 메서드로 구현한다. 그리고 이를 캡슐화하여 디커플링을 도모한다.


인터페이스의 메서드는 각 상태에서 수행해야 하는 행위들이며, 상태 변경은 상태 스스로 알아서 다음 상태를 결정한다. 


경우에 따라 상태 변경을 관리하는 클래스를 따로 만드는 방법도 생각해볼 수 있는데, 이런 경우 각 상태 클래스만 보고서는 해당 상태 후에 어떤 상태로 변경되는지 알 수 없고, 상태 변경을 관리하는 클래스를 살펴봐야 한다.




7. 장단점


장점 : 복잡한 if - else if - else 문 / swtich문을 줄임, 따라서 가독성 및 유연성 증가로 인한 유지보수성 향상

단점 : 


'Design Pattern > Behavioral Pattern' 카테고리의 다른 글

Template Method Pattern (기본)  (0) 2017.09.26
Observer Pattern  (0) 2017.09.26
Command Pattern  (0) 2017.09.26
State Pattern과 Strategy Pattern의 공통점과 차이점  (0) 2017.09.26
Strategy Pattern  (0) 2017.09.26
Comments