일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- heroku
- 자바입력
- 자바 스레드 실행 순서 제어
- R
- Kadane's Algorithm
- SpringBoot 2
- input
- array
- 수학
- 사칙연산
- Easy
- scanner
- JAVA11
- hash table
- 카데인 알고리즘
- 자바 thread 실행 순서 제어
- Today
- Total
DeFacto-Standard IT
State Pattern 본문
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 |