디자인패턴 - 스테이트 패턴(State Pattern),상태 패턴

2019. 8. 15. 17:21프로그래밍언어/디자인패턴

state의 의미는 '상태'이다. 엘리베이터의 정지, 하강, 상승 상태처럼 객체 상태도 상황에 따라 달라진다. state 패턴은 동일한 동작을 객체 상태에 따라 다르게 처리해야 할 때 사용한다.

이렇게 하나의 객체에 여러 가지 상태(예, 정지, 상승, 하강)가 존재할 때 패턴을 사용하지 않고 프로그래밍을 하면 if 문 또는 switch 문을 사용하여 처리한다. 그런데 신규 상태(예, 문 열림, 문 닫힘)가 발생하면 프로그램을 다시 수정해야 한다.

이런 경우에 state 패턴은 그림처럼 객체 상태를 캡슐화하여 클래스화(state 인터페이스)함으로써 그것을 참조하게 하는 방식으로 상태에 따라 다르게 처리(upState, stopState, downState)할 수 있도록 한 것이다. 따라서 변경 시(신규 상태 추가) 원시 코드의 수정을 최소화할 수 있고, 유지보수를 쉽게 할 수 있다.

 

형광등을 예로 들어보자. 형광등의 첫 상태는 스위치가 꺼진 OFF 상태라고 생각하자. 상태가 OFF인 상태에서 스위치를 ON하면 불이 켜질 것이다. 그렇지만 상태가 ON인 상태에서 스위치를 ON하면 이미 불이 켜진 상태라 아무런 동작을 하지 않을 것이다. 그 반대도 역시 동일하다. 그렇다면 이렇게 동작하는 형광등 클래스는 어떻게 설계하면 좋을까? 가장 간단한 구현은 아래와 같을 것이다.

 

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
public class NotStatePattern {
    
    public static final int ON=1;
    public static final int OFF=0;
    
    //현 상태
    private int currentState;
    
    public NotStatePattern() {
        currentState=OFF;
    }
    
    //ON 스위치 push
    public void on_button() {
        if(currentState==ON) System.out.println("동작 없음.");
        else {
            System.out.println("불 켜짐!");
            currentState=ON;
        }
    }
    //OFF 스위치 push    
    public void off_button() {
        if(currentState==OFF) System.out.println("동작 없음.");
        else {
            System.out.println("불 꺼짐!");
            currentState=OFF;
        }
    }
    
}
cs

 

ON,OFF 상태값을 가지는 상수와 현재 형광등의 상태를 가지고 있는 변수. 그리고 스위치를 ON,OFF할때 발생되는 행위를 메소드로 구현해놓았다. 얼핏 봤을 때, 나쁘지 않은 구현일 수 있다. 하지만 예를 들어 수면모드가 들어간 형광등이라면? 스위치가 ON인 상태에서 다시 한번 ON 스위치를 누를 경우 수면 모드로 들어간다. 그리고 수면 모드에서 OFF 스위치를 누르면 형광등이 꺼진다. 만약 복잡한 시스템이라면 이러한 상태값은 더 많이 갖게 될 것이고, 그럼 많은 상태에 따른 행위를 위와 같이 if문 혹은 switch 문으로 다뤄야할 것이며 이는 상태가 추가될때 마다 코드가 수정되야하고 가독성도 떨어지며 최종적으로는 유지보수가 힘든 코드가 될 것이다.

 

이러한 상황에서 사용할 수 있는 디자인 패턴이 스테이트 패턴(State Pattern)이다. 아마 예제 코드를 본다면 우리 이미 다루어본 스트레티지 패턴(전략패턴)과 비슷하다 볼 수 있을 것이다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Light {
    
    private State state;
    
    public Light() {
        state=OffState.getInstance();
    }
    
    public void setState(State state) {
        this.state=state;
    }
    
    public void onButton() {
        this.state.onButton(this);
    }
    
    public void offButton() {
        this.state.offButton(this);
    }
    
}
cs

 

형광등 클래스이다. 마치 스트레티지 패턴의 Context 객체와 비슷하다. 즉, 해당 객체는 실제 알고리즘을 수행하는 실체 객체를 알지못한다. 단순히 남에게 행위를 위임해주는 역할이다.

 

1
2
3
4
5
6
public interface State {
    
    public void onButton(Light light);
    public void offButton(Light light);
    
}
cs

 

디자인 원칙을 떠올려보자. 변하는 것은 잘 변하지 않는 것과 분리해라. 즉, 변하는 녀석들을 캡슐화해라! 

 

우리는 변하는 것은 상태(State)라는 것을 알고있다. 이러한 상태를 인터페이스로 분리시켰고 각 상태에 따른 구현 클래스를 만들어 줄 것이다. 여기서 스트래티지 패턴과 조금 다른 점은 Light(Context)객체가 행위를 위임할때 자신의 상태를 변경하기 위하여 자기자신을 메소드 매개변수로 넘기는 것을 주목하자! 차이점은 마지막에 다룰 것이다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class OnState implements State {
    
    private OnState() {}
    
    private static class LazyHolder{
        public static final OnState ON=new OnState();
    }
    
    public static OnState getInstance() {
        return LazyHolder.ON;
    }
 
    @Override
    public void onButton(Light light) {
        light.setState(SleepState.getInstance());
        System.out.println("잠자기 모드!");
    }
 
    @Override
    public void offButton(Light light) {
        light.setState(OffState.getInstance());
        System.out.println("불 꺼짐!");
    }
 
}
 
public class OffState implements State {
    
    private OffState() {}
    
    private static class LazyHolder{
        public static final OffState OFF=new OffState();
    }
    
    public static OffState getInstance() {
        return LazyHolder.OFF;
    }
    
    @Override
    public void onButton(Light light) {
        light.setState(OnState.getInstance());
        System.out.println("불 켜짐!");
    }
 
    @Override
    public void offButton(Light light) {
        System.out.println("동작 없음!");
    }
 
}
 
public class SleepState implements State {
    
    private SleepState() {}
    
    private static class LazyHolder{
        public static final SleepState SLEEP=new SleepState();
    }
    
    public static SleepState getInstance() {
        return LazyHolder.SLEEP;
    }
    
    @Override
    public void onButton(Light light) {
        light.setState(OnState.getInstance());
        System.out.println("잠자기 모드 해제!(ON 상태)");
    }
 
    @Override
    public void offButton(Light light) {
        light.setState(OffState.getInstance());
        System.out.println("불 꺼짐!");
    }
 
}
cs

 

각 상태들의 구현체이다. 여기서 조금 특이한 것은 각 상태의 구현체들이 싱글톤으로 구현되어 있다는 것이다. 이것은 상태들이 행위를 수행하면서 Light 객체의 상태를 수시로 바꾸어주기 때문에 싱글톤으로 작성하지 않으면 매번 새로운 인스턴스가 생겨 불필요한 메모리를 잡아 먹을 것이고 전체적으로 성능 저하의 원인이 될 것이기 때문에 싱글톤으로 작성하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class StatePatterMain {
 
    public static void main(String[] args) {
        Light l = new Light();
        
        l.offButton();
        l.onButton();
        l.onButton();
        l.onButton();
        l.offButton();
        l.onButton();
        l.offButton();
    }
 
}
 
=>result
동작 없음!
불 켜짐!
잠자기 모드!
잠자기 모드 해제!(ON 상태)
불 꺼짐!
불 켜짐!
불 꺼짐!
cs

 

마지막 결과이다. 잘 보면 스트래티지 패턴과 차이점이 보일 것이다. 스트래티지 패턴을 떠올려보면 클라이언트 쪽에서 알고리즘을 변경하기 위하여 setter를 호출해 직접 수행할 알고리즘을 주입해주었던 것을 기억할 것이다. 즉, 클라이언트가 구체적인 알고리즘의 수행까지는 몰라도 어느정도 무엇무엇이 있는지 정도는 알고 있어야 한다는 것이다. 하지만 스테이트 패턴은 각 상태 구현 클래스들이 자신들의 행위를 수행하면서 직접 Context(Light)객체의 상태를 변경해주기 때문에 클라이언트 입장에서는 직접 상태를 조작하거나 하지 않아도 된다는 점이다. 즉, 클라이언트는 상태를 몰라도 된다라는 뜻이다.(전략을 직접 클라이언트가 바꿔서 사용해야하는 스트래티지 패턴과는 조금은 상반된다.)

 

즉, 용도가 조금 다른 패턴이라고 볼 수 있다.

 

스트래지티 패턴 - 사용자가 쉽게 알고리즘 전략을 바꿀 수 있도록 유연성을 제공. 상속의 한계를 해결하기 위하여 나온 패턴

스테이트 패턴 - 한 객체가 동일한 동작을 상태에 따라 다르게 수행해야 할 경우 사용하는 패턴