스테이트 패턴(State Pattern) ; 캡슐화, OCP

객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있다.
이를 통해 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.

책에서 나오는 예제 - 뽑기기계 소프트웨어 만들기

뽑기 기계 예시가 나온다. 각 4가지 상태가 있고, 4가지 행동이 조건으로 주어진다.
최초에는 상태를 멤버 변수로 지정 하고, 각 변수 값에 따라 조건문으로 행동을 제어한다.

이렇게 구현하면, OCP를 위배하게 되고 객체지향 디자인이라고 보긴 어렵다.
역시나 ‘바뀌는 부분을 별도 책임으로 설계하고, 캡슐화 한다.’ 라는 디자인 패턴을 고려한다.

  1. 따라서 State 라는 인터페이스로 행동을 추상화 하고,
  2. 모든 상태에 대해 State를 구현한다
  3. 조건문을 없애고, State에 이를 위임한다.

State Interface

시스템에서 나올 수 있는 행동을 추상화 한 인터페이스

1
2
3
4
5
6
7
8
9
public interface State {
void insertQuarter();

void ejectQuarter();

void turnCrank();

void dispense();
}

GumballMachine Class

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
@Getter
public class GumballMachine {
private State soldOutState;
private State noQuarterState;
private State hasQuarterState;
private State soldState;
private State state = soldOutState;
private int count = 0;

public GumballMachine(int numberGumballs) {
this.soldOutState = new SoldOutState(this);
this.noQuarterState = new NoQuarterState(this);
this.hasQuarterState = new HasQuarterState(this);
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 turnCrank() {
state.turnCrank();
state.dispense();
}

public void releaseBall() {
System.out.println("a gumball comes rolling out the slot...");
if (count != 0) {
count -= 1;
}
}

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

public void setCount(int count) {
this.count = count;
}
}

동전 있음 상태 class (HasQuarter)

동전이 있는 상태에서 각 행동에 대한 구현체

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
public class HasQuarterState implements State {
private final GumballMachine gumballMachine;

public HasQuarterState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}

@Override
public void insertQuarter() {
System.out.println("동전은 한 개만 넣어주세요");
}

@Override
public void ejectQuarter() {
System.out.println("동전이 반환됩니다.");
gumballMachine.setState(gumballMachine.getNoQuarterState());
}

@Override
public void turnCrank() {
System.out.println("손잡이를 돌리셨습니다.");
gumballMachine.setState(gumballMachine.getSoldState());
}

@Override
public void dispense() {
System.out.println("알맹이가 나갈 수 없습니다.");
}
}

생각해보자

상태 패턴은 사용자로 하여금 동일한 요청에 대해 시스템이 갖고 있는 상태에 따라 다른 반환 값을 보여준다는 점에서 굉장히 유연한 설계가 될 수 있다고 생각했다.

‘상태라는 것이 태생적으로 공유자원에 속하기 때문에, 상태를 객체로 관리하면 동시성 문제가 발생 할 여지가 있겠다..’ 라는 생각이 들었다.
동시성 문제를 해결 혹은 방지하기 위해 여러가지 해결법이 이미 많이 나와있지만, 가장 편한 것은 역시 DB에서 상태 값으로 관리하는 것이라는 생각이 들었다.
그래서 상태 패턴을 적용한 코드를 찾아보기 힘들었던 것이 아닐까..?

또한, 상당히 오래된 코드이다보니 상태값을 setState(newState); 라는 메소드로 변경하는 것에 대해 굉장히 불편한 생각이 들었다.
change**State() 라는 별도의 메소드로 관리하면 어떨까?

그간 책에 나와있는 코드들을 마치 진리로 자연스럽게 받아들이곤 했는데, ‘불편하다’라는 생각이 들었다는 것은
한편으로 보면 자만일 수도 있고, 내가 조금은 성장하진 않았을까 생각이 번갈아가면서 들었다.
앞으론 책에 나와있는 코드도 조금은 비판적으로(비난) 보는 습관을 들여야겠다.

참고자료