실세계의 많은개체는 자신이 처한 상태에 따라 일을 다르게 수행한다. 비가 오거나 눈이 오거나 사람이 많이 붐비는 장소에 있거나 따라 걷는 방식과 말하는 방식이 달리지는 것과 마찬가지인 이치다.
이를 표현하는 가장 직접적이고 직관적인 방법은 이를 수행할 때의 상태에 따라 상태 하나하나를 검사해 일을 다르게 수행하게끔 하는 것이다. 이는 분명히 복잡한 조건식이 있는 코드를 산출할 것이고, 결과적으로 코드를 이해하거나 수정하기 어렵게 만든다.
이런 방식과 달리 State패턴은 어떤 행위를 수행할 때 상태에 행위를 수행하도록 위임한다. 이를 위해 State패턴에서는 시스템의 각 상태를 클래스로 분리해 표현하고, 각 클래스에서 수행하는 행위들을 메소드로 구현한다. 그리고 이러한 상태들을 외부로부터 캡슐화 하기 위해 인터페이스를 만들어 시스템의 각 상태를 나타내는 클래스로 하여금 실체화하게 한다.
상태머신 다이어그램을 기반으로한 코드
public class Lamp {
private static int ON = 1; // 형광등이 켜진 상태
private static int OFF = 0; // 형광등이 꺼진 상채
private int state; // 형광등의 현재 상채
public Lamp() {
state = OFF;
}
public void onButtonPush() {
if (state == ON) {
System.out.println("-- No affect --");
}
if (state == OFF) {
System.out.println(" !! Lamp on !!");
state = ON;
}
}
public void offButtonPush() {
if (state == OFF) {
System.out.println("-- No affect --");
}
if (state == ON) {
System.out.println(" !! Lamp off !!");
state = OFF;
}
}
}
그리고 아래는 실행 코드다.
public class LampTest {
public static void main(String[] args) {
Lamp lamp = new Lamp();
lamp.onButtonPush();
lamp.onButtonPush();
lamp.offButtonPush();
lamp.offButtonPush();
}
}
문제점
현재 형광등에 새로운 상태인 ‘취침등’ 상태를 추가하려면?
0과 1외에 취침등의 상태 상수를 추가하고 onButtonPush와 offButtonPush의 상태를 처리할 코드를 작성해야한다.
아래는 작성된 코드다
public class Lamp {
private static int ON = 1; // 형광등이 켜진 상태
private static int OFF = 0; // 형광등이 꺼진 상채
private static int SLEEPING = 2; // 추가된 조건
private int state; // 형광등의 현재 상채
public Lamp() {
state = OFF;
}
public void onButtonPush() {
if (state == ON) {
System.out.println("-- No affect --");
} else if (state == SLEEPING) {
System.out.println("!! Lamp on !!");
state = ON;
} else {
System.out.println(" !! Lamp on !!");
state = ON;
}
}
public void offButtonPush() {
if (state == OFF) {
System.out.println("-- No affect --");
} else if (state == SLEEPING) {
System.out.println("!! Lamp off !!");
state = OFF;
} else {
System.out.println(" !! Lamp off !!");
state = OFF;
}
}
}
보다시피 복잡한 조건문으로 상태 진입이 내포된 지금의 코드는 구조를 파악하기 쉽지 않다. 그리고 새로운 상태가 추가될 때 마다 상태 변화를 초래하는 모든 메소드에 이를 반영하기 위해 코드를 수정해야만 한다.
해결책
변하는 부분을 찾아서 이를 캡슐화 하는 것이 목표다. 현재 상태에 상관없이 구성하고 변화되더라도 독립적이도록 코드를 만들자.
이를 위해서 강태를 클래스로 분리해 캡슐화 하도록 한다. 상태에 의존적인 행위들도 클래스에 같이 둬 특정 상태에 따른 행위를 구현하도록 한다. 이렇게 하면 상태에 따른 행위가 각 클래스에 국지화되어 이해하고 수정하기가 쉽다.
Strategy 패턴과 동일하다. Lamp클래스에 구체적인 상태 클래스가 아닌 추상화된 State인터페이스만 참조하므로 현재 어떤 상태에 있는 지와 무관하게 코드를 작성할 수 있다. Lamp 클래스에서는 상태 클래스에 작업을 위임만 하면 된다.
public interface State {
public void onButtonPushed(Lamp lamp);
public void offButtonPushed(Lamp lamp);
}
public class On implements State {
private static On on = new On();
private On() {
}
public static On getInstance() {
return on;
}
@Override
public void onButtonPushed(Lamp lamp) {
System.out.println("-- no affect --");
}
@Override
public void offButtonPushed(Lamp lamp) {
System.out.println("!! Lamp off !!");
lamp.setState(Off.getInstance());
}
}
public class Off implements State {
private static Off off = new Off();
private Off() {
}
public static Off getInstance() {
return off;
}
@Override
public void onButtonPushed(Lamp lamp) {
System.out.println("!! Lamp on !!");
lamp.setState(On.getInstance());
}
@Override
public void offButtonPushed(Lamp lamp) {
System.out.println("-- no affect --");
}
}
Lamp클래스의 상태인 On과 Off 클래스를 캡슐화해 State 인터페이스를 구현했다.
이렇게 캡슐화 하면 클래스만 보더라도 각 상태에 따라 메소드가 어떻게 작동하는지 쉽게 알 수 있다. 또한 상태 진입도 상태에서 처리하므로 if나 switch문이 필요없다.
public class Lamp {
private State state;
public Lamp() {
state = Off.getInstance();
}
public void setState(State state) {
this.state = state;
}
public void onButtonPush() {
state.onButtonPushed(this);
}
public void offButtonPush() {
state.offButtonPushed(this);
}
}
Lamp클래스의 state 변수를 통해 현재 시스템의 각 상태 객체를 참조한다.
상태에 따른 행위를 수행하려면 state변수가 참조하는 상태 객체에 작업을 위임해야한다. Lamp코드 어디를 보더라도 구체적인 상태를 나타내는 객체를 참조하지 않는다. 즉, Lamp클래스는 시스템이 어떤 상태에서 있는지 무관하다는 의미다.
Strategy 패턴에서 다른 방식의 전략을 사용했을때도 이를 이용하는 Context 클래스는 전혀 영향을 받지 않음을 상기할 필요가 있다.