Design Pattern

State 디자인 패턴 (상태를 클래스로)

태인킴 2023. 1. 23. 13:11
반응형

State 패턴 (상태를 클래스로)
State 패턴 (상태를 클래스로)


state 디자인 패턴은 '상태', 사물의 모양이나 형태를 '클래스' 표현합니다.

  • 클래스를 교체해서 '상태의 변화'를 표현할 수 있고
  • 새로운 상태를 추가해야 될 때, 해당 상태에 해당하는 클래스만 정의해 주면 되는 패턴입니다.
목차

1. State 디자인 패턴을 활용한 예제

2. State 패턴을 사용하지 않은 방법

3. State 패턴을 사용한 방법

4. State 패턴의 등장인물

4-1. State 인터페이스

4-2. Concrete State 클래스1 (DayState)

4-3. Concrete State 클래스2 (NigntState)

4-4. Context 인터페이스

4-5. SafeFrame 클래스 (Context 구현 클래스)

4-6. Main 클래스

5. 분할해서 통치해라

6. 상태에 의존한 처리

7. 상태 전환은 누가 관리해야하는가?

8. 결론

 

 

1. State 디자인 패턴을 활용한 예제

예를 들어, 금고경비 시스템을 개발한다고 할 때, 시간마다 경비 상태가 변화하는 금고 시스템입니다.

 

요구사항 (금고경비 시스템)
- 금고가 1개 있다.
- 금고는 경비센터와 접속되어 있다.
- 금고에는 비상벨과 일반통화용 전화가 접속되어 있다.
- 금고에는 시계가 설치되어 있어 현재의 시간을 감시하고 있다.
- 주간은 9:00 ~ 16:59
- 야간은 17:00 ~ 23:59 및 0:00 ~ 8:59
- 금고는 주간에만 사용할 수 있다.
- 주간에 금고를 사용하면 경비센터에 사용기록이 남는다.
- 야간에 금고를 사용하면 경비센터에 비상사태로 통보가 된다.
- 비상벨은 언제나 사용할 수 있다.
- 비상벨을 사용하면 경비센터에 비상벨 통보가 된다.
- 일반통화용의 전화는 언제나 사용할 수 있다.(그러나 야간은 녹음만 가능)
- 주간에 전화를 사용하면 경비센터가 호출된다.
- 야간에 전화를 사용하면 경비센터의 자동응답기가 호출된다.

요구사항 시스템 구성도
요구사항 시스템 구성도

 

 

2. State 패턴을 사용하지 않은 방법

위 예제를 State 패턴을 사용하지 않고, 코딩을 하게 되면 다음과 같이 개발하는 경우가 많습니다.

경비시스템의 클래스 {
	금고 사용시에 호출되는 메소드() {
    	if(주간) {
            경비센터에 이용 기록
        } else if(야간) {
            경비센터에 비상사태 통보
        }
    }
    
  	비상벨 사용시에 호출되는 메소드() {
        경비센터에 비상벨 통보
    }
    
    일반 통화시에 호출되는 메소드() {
    	if(주간) {
            경비센터의 호출
        } else if(야간) {
            경비센터의 자동응답기 호출
        }
    }
}

위와 같이 if문을 활용한 분기 로직이 여러개 생기면서 논리를 읽기가 어려워지고, 관리하기 어려워집니다. 여기서 State 패턴은 다른 관점으로 접근을 합니다.

 

 

3. State 패턴을 사용한 방법

반면에 State 패턴을 사용하게 되면 '주간', '야간'과 같은 상태를 클래스로 표현하면서, 상태 검사를 위한 if문이 사라집니다. 

 

이름 내용
State 금고의 상태를 나타내는 인터페이스
DayState 주간의 상태를 나타내는 클래스
NightState 야간의 상태를 나타내는 클래스
Context 금고의 상태변화를 관리하는 인터페이스
SafeFrame 버튼이나 화면표시 등의 사용자 인터페이스를 갖는 클래스
Main 동작 테스트용 클래스
'디자인 패턴'의 다른 글
• 템플리 메소드 패턴
• 생산자 소비자 패턴

 

 

4. State 패턴의 등장인물

 등장인물 역할
State State 인터페이스는 상태를 나타냅니다. 상태가 변할 때마다 다른 동작을 하는 인터페이스를 결정합니다. 이 인터페이스는 '상태에 의존한 동작'을 하는 메소드의 집합 입니다.
Concrete State (구체적인 상태) Concrete State 역할은 구체적인 각각의 '상태를 표현'합니다. 예제에서는 DayState, NightState 클래스가 이 역할을 합니다.
Context (문맥) Context 역할은 '현재의 상태를 관리'하는 역할입니다. State를 멤버 변수로 가지고 있습니다. 예제에서는 Context 인터페이스와 SafeFrame 클래스가 이 역할을 합니다.

 

classDiagram
Context o--> State
State <|-- ConcreteState1
State <|-- ConcreteState2
Context : state
Context : requestX()
Context : requestY()
Context : requestZ()
State : methodA()
State : methodB()
State : methodC()
State : methodD()
ConcreteState1:methodA()
ConcreteState1:methodB()
ConcreteState1:methodC()
ConcreteState1:methodD()
ConcreteState2:methodA()
ConcreteState2:methodB()
ConcreteState2:methodC()
ConcreteState2:methodD()

 

 

4-1. State 인터페이스

State 인터페이스는 '금고 상태의 의존된 메소드 집합'입니다.

  • 시간이 설정되었을 때
  • 금고가 사용되었을 때
  • 비상벨이 눌렸을 때
  • 일반통화를 할 때

 

이 메소드는 모두 상태에 따라서 로직이 변하게 됩니다. 인수로 전달되고 있는 Context는 상태의 관리를 수행하고 있는 인터페이스 입니다.

public interface State {
   public abstract void doClock(Context context, int hour); // 시간 설정
   public abstract void doUse(Context context);             // 금고 사용
   public abstract void doAlarm(Context context);           // 비상벨
   public abstract void doPhone(Context context);           // 일반통화
}

 

 

4-2. Concrete State 클래스 (DayState)

DayState 클래스는 State 인터페이스를 구현하고 있는 구체적인 상태, '주간'의 상태를 나타내는 클래스입니다. doClock 메소드는 시간을 설정하는 메소드입니다. 여기에서 '상태의 변화'가 일어납니다. doUse, doAlarm, doPhone 이들 메소드 안에 '현재의 상태를 검사' 하는 if문이 없는 점을 확인하세요. 이 메소드들은 '현재는 주간의 상태'라는 점만 생각하고 로직을 구현하면 됩니다. State패턴에서는 상태의 차이가 클래스의 차이로 표현되기 때문에, if문이나 switch문으로 상태가 변할 때마다 분기할 필요가 없습니다. 그리고, 상태가 변화할 때마다 새로운 인스턴스를 만들게 되면, 메모리와 시간이 낭비되기 때문입니다. 따라서 Singleton 패턴을 사용합니다.

public class DayState implements State {
    private static DayState singleton = new DayState();
    private DayState() {                             //생성자는 private (싱글톤 패턴)
    }
    public static State getInstance() {              //유일한 인스턴스를 얻는다.(싱글톤 패턴)
    	return singleton;
    }
    public void doClock(Context context, int hour) { //시간 설정
    	if(hour < 9 || 17 <= hour) {
        	context.changeState(NightState.getInstance());
        }
    }
    public void doUse(Context context) {             //금고 사용
    	context.recordLog("금고사용 (주간)");
    }
    public void doAlarm(Context context) {            //비상벨
    	context.callSecurityCenter("비상벨 (주간)");
    }
    public void doPhone(Context context) {            //일반통화
    	context.callSecurityCenter("일반통화 (주간)");
    }
    public String toString()() {            
    	return "[주간]";
    }
}

 

 

4-3. Concrete State 클래스 (NightState)

NightState 클래스는 '야간 상태'를 나타내는 클래스입니다.

public class NightState implements State {
    private static NightState singleton = new NightState();
    private NightState() {                           //생성자는 private
    }
    public static State getInstance() {              //유일한 인스턴스를 얻는다.
    	return singleton;
    }
    public void doClock(Context context, int hour) { //시간 설정
    	if(9 <= hour && hoour < 17) {
            context.changeState(DayState.getInstance());
        }
    }
    public void doUse(Context context) {             //금고 사용
    	context.callSecurityCenter("비상 : 야간금고 사용!)");
    }
    public void doAlarm(Context context) {            //비상벨
    	context.callSecurityCenter("비상벨(야간)");
    }
    public void doPhone(Context context) {            //일반통화
    	context.recordLog("야간통화 녹음");
    }
    public String toString()() {            
    	return "[야간]";
    }
}

 

 

4-4. Context 인터페이스

context 인터페이스는 '상태를 관리'하거나 경비센터의 호출을 수행합니다.

public interface Context {
    public abstract void setClock(int hour);             //시간설정
    public abstract void changeState(State state);       //상태전환
    public abstract void callSecurityCenter(String msg); //경비센터 호출
    public abstract void recordLog(String msg);          //경비센터 기록
}

 

 

4-5. SafeFrame 클래스 (Context를 구현한 클래스)

(SafeFrame => '대충 금고라는 뜻') SafeFrame 클래스는 GUI 역할을 합니다. SafeFrame 클래스는 Context 인터페이스를 구현합니다. SafeFrame 클래스의 필드 중 State 필드'금고의 현재 상태'를 나타냅니다. 처음에는 '주간'의 상태로 초기화되어 있습니다.

public class SafeFrame extends Frame implements ActionListener, Context {
    private TextField textClock = new TextField(60);     //현재시간 표시
    private TextArea textScreen = new TextArea(10, 60);  //경비센터 출력
    private Button buttonUse = new Button("금고사용");   
    private Button buttonAlarm = new Button("비상벨");   
    private Button buttonPhone = new Button("일반통화"); 
    private Button buttonExit = new Button("종료");      

    private State state = DayState.getInstance();      //현재의 상태
    
    public SafeFrame(String title) {
    	.....
        //listener 설정
        buttonUse.addActionListener(this);
        buttonAlarm.addActionListener(this);
        buttonPhone.addActionListener(this);
        buttonExit.addActionListener(this);
    }
    
    public void actionPerformed(ActionEvent e) {
    	System.out.println(e.toString());
        if (e.getSource() == buttonUse) {         //금고사용 버튼
        	state.doUse(this);
        } else if(e.getSource() == buttonAlarm) { //비상벨 버튼
        	state.doAlarm(this);
        } else if(e.getSource() == buttonPhone) { //일반통화 버튼
        	state.doPhone(this);
        } else if(e.getSource() == buttonExit) {  //종료 버튼
        	System.exit(0);
        } else {
        	System.out.println("?");
        }
    }
    
    //시간설정
    public void setClock(int hour) {
    	String clockstring = "현재 시간은";
        if (hour < 10) {
        	clockstring += "0" + hour + ":00";
        } else {
        	clockstring += hour + ":00";
        }
        System.out.println(clockstring);
        textClock.setText(clockstring);
        state.doClock(this, hour); //시간 설정
    }
    
    //상태전환
    public void changeState(State state) {
    	System.out.println(this.state + "에서" + state + "로 상태가 변화했습니다.");
        this.state = state;
    }
    
    //경비센터의 호출
    public void callSecurityCenter (String  msg) {
    	textScreen.append("call! " + msg + "\n");
    }
    
    //경비센터의 기록
    public void recordLog(String msg) {
    	textScreen.append("record... " + msg + "\n");
    }
}

시퀀스 다이어그램으로 표시하면 아래와 같습니다. '상태 변화'의 전, 후에 doUse()를 실행하고 있는 모습입니다. 처음에는 DayState 쪽의 doUse()가 호출되고 있지만, changeState() 한 후에는 NightState 쪽의 doUse()가 호출되고 있습니다. changeState() 후에 상태가 변화한 모습입니다.

sequenceDiagram
  participant M as Main
  participant A as ActionListener
  participant S as :SafeFrame
  participant D as :DayState
  participant N as :NightState
  A->>+S: actionPerformed 
  S->>+D: doUse 
  D--)-S: -doUse 
  S--)-A: -actionPerformed  
  M->>+S: setClock
  S->>+D: doClock
  D->>+S: changeState
  S--)-D: -changeState  
  D--)-S: -doClock
  S--)-M: -setClock  
  A->>+S: actionPerformed
  S->>+N: doUse
  N--)-S: -doUse
  S--)-A: -actionPerformed 

 

 

4-6. Main 클래스

Main 클래스는 SafeFrame.setClock() 메서드를 정기적으로 호출하여 시간을 설정합니다.

public class Main {
    public static void main(String[] args) {
        SafeFrame frame = new SafeFrame("Safe Frame App");
            while (true) {
                for (int hour = 0; hour < 24; hour++) {
                    frame.setClock(hour);   //시간 설정
                    try {
                        Thread.sleep(1000); //1초 시간 경과
                    } catch(InterruptedException e) {
                        e.printStacktrace();
                    }
                }
            }
    }
}

 

 

5. 분할해서 통치해라(divide and conquer)

divide and conquer는 분할 정복 알고리즘으로 유명합니다. 규모가 크고 복잡한 문제를 우선, 작은 문제로 나누어서 해결하는 알고리즘 입니다. State 패턴에서는 '상태'를 클래스로 표현했습니다. 각각의 구체적인 상태를 각각의 클래스로 표현해서 문제를 분할한 것입니다. 예제에서는 DayState, NightState 처럼 상태가 2가지 밖에 없었지만, 상태가 많을 경우 State 패턴의 장점을 발휘할 수 있습니다. 만약, State 패턴을 사용하지 않고, 상태가 많으면 많을수록 이 조건문도 증가합니다. 그리고 이런 비슷한 조건문을 이벤트마다 기술해야 합니다.

 

 

6. 상태에 의존한 처리

SafeFrame.setClock()과 State.doClock()의 관계에 대해서 생각해 봅시다.

호출은 다음과 같이 이루어집니다.

Main → SafeFrame.setClock() → state.doClock()

SafeFrame은 state에 위임을 하고 있습니다. state 인터페이스로 선언되고 있는 메소드는 모두 '상태에 의존한 처리'입니다. state.doClock()에서 실질적인 상태 변화가 이루어지므로, '상태 변화' 역시, '상태에 의존한 처리' 임을 의미합니다.

 

 

7. 상태전환은 누가 관리해야 하는가?

예제에서는 Context 역할의 SafeFrame 클래스가 상태전환을 수행하는 changeState() 메소드를 구현합니다. (Context)SafeFrame.changeState() → DayState.doClock()에서 상태전환을 수행합니다. 즉, '상태변화'를 '상태에 의존한 로직'으로 간주하고 있습니다. 왜냐하면 '상태에 의존한 로직'만 Concrete State에 들어갈 수 있기 때문입니다.

  • '장점'은, '다른 상태로 전환하는 것은 언제인가'하는 정보가 하나의 클래스 내에 정리되어 있는 점입니다.
  • '단점'은 하나의 Concrete State 역할이 다른 Concrete State 역할을 알아야 한다는 점입니다.

 

예를 들어 DayState.doClock() 안에서 NightState 클래스를 호출하고 있습니다. 이것은 추후에 NightState 클래스를 삭제하게되면, DayState 클래스도 수정해야 합니다. 즉, 상태전환을 Concrete 역할에 맡기면 클래스 사이의 의존관계가 깊어집니다. '상태전환' 로직은 다음과 같이 넣을 수 있습니다.

  • '상태전환'이 상태에 의존한 로직이라면, Concrete State클래스에 맡길 수 있습니다. (예제의 경우)
  • 모든 '상태전환'을 Context(SafeFrame) 클래스에 맡길 수 있습니다.
  • 또는 Mediator 패턴을 사용하거나,
  • State 패턴 대신에 상태 테이블(표)을 사용해서 설계하는 방법도 있습니다.
  • 상태수가 많으면, 상태가 변경될 때, 프로그램을 자동 생성하는 다른 프로그램을 사용하는 방법

 

모든 '상태전환'을 Context 클래스에 맡기면, 각각의 Concrete State에 독립성이 좋아지는 대신, Context가 모든 Concrete State를 알아야 하는 단점이 생깁니다. 그리고, 상태 테이블을 이용한 방법은 '입력과 내부 상태', '출력과 다음 내부상태'를 얻을 수 있는 테이블입니다. 상태전환이 일정한 규칙이 있는 경우에는 테이블 자료구조를 활용할 수도 있습니다.

 

 

8. 결론

  • State 패턴은 상태를 클래스로 표현하였기 때문에, State 인터페이스를 구현한 클래스를 만들어주면 되기 때문입니다. 
  • State 인터페이스는 '상태에 의존한 동작' 메소드 모음입니다.
  • 동시에 여러 상태가 되어서는 안됩니다. DayState 상태이면서, NightState 상태가 될 수는 없습니다.
  • '상태 전환' 로직을 어느 곳에 위치하냐에 따라서, 의존성을 주의해야 합니다.

 

이 글이 도움이 되었다면, 좋아요를 눌러주세요. 주변에 이 정보가 필요한 사람이 있다면, 공유해주세요.

반응형

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

생산자(Producer) 소비자(Consumer) 패턴  (0) 2020.08.23
템플릿 메서드(Template Method) 패턴  (0) 2020.08.18