스테이트 패턴 ( State Pattern )
스테이트 패턴은 객체가 특정 상태에 따라 행위를 달리하는 상황에서,
자신이 직접 상태를 체크하여 상태에 따라 행위를 호출하지 않고,
상태를 객체화 하여 상태가 행동을 할 수 있도록 위임하는 패턴을 말합니다.
즉, 객체의 특정 상태를 클래스로 선언하고, 클래스에서는 해당 상태에서 할 수 있는 행위들을 메서드로 정의합니다.
그리고 이러한 각 상태 클래스들을 인터페이스로 캡슐화 하여, 클라이언트에서 인터페이스를 호출하는 방식을 말합니다.
1. 스테이트 패턴 사용 이유
예를 들어, 노트북을 켜고 끄는 상황을 생각해보겠습니다.
- 노트북 전원이 켜져 있는 상태에서 전원 버튼을 누르면, 전원을 끌 수 있습니다.
- 노트북 전원이 꺼져 있는 상태에서 전원 버튼을 누르면, 전원을 켤 수 있습니다.
이런 행위를 할 수 있는 Laptop 클래스는 다음과 같이 정의할 수 있습니다.
public class Laptop {
public static String ON = "on";
public static String OFF = "off";
private String powerState = "";
public Laptop(){
setPowerState(Laptop.OFF);
}
public void setPowerState(String powerState){
this.powerState = powerState;
}
public void powerPush(){
if ("on".equals(this.powerState)) {
System.out.println("전원 off");
}
else {
System.out.println("전원 on");
}
}
}
public class Client {
public static void main(String args[]){
Laptop laptop = new Laptop();
laptop.powerPush();
laptop.setPowerState(Laptop.ON);
laptop.powerPush();
}
}
노트북이 on 상태이면 노트북 상태를 off로 변경하고, 상태가 off이면, on으로 변경하는 간단한 코드입니다.
그런데 간단하게 켜고, 끄는 노트북에 절전모드를 추가한다고 해보겠습니다.
상태에 따른 동작은 다음과 같다고 가정합니다.
- 노트북 전원이 켜져 있는 상태에서 전원 버튼을 누르면, 전원을 끌 수 있습니다.
- 노트북 전원이 꺼져 있는 상태에서 전원 버튼을 누르면, 절전모드가 됩니다.
- 노트북 절전모드 상태에서 전원 버튼을 누르면, 전원을 켤 수 있습니다.
이렇게 절전모드가 추가된 Laptop 클래스는 다음과 같이 조건문이 하나 더 추가됩니다.
public class Laptop {
public static String ON = "on";
public static String OFF = "off";
public static String SAVING = "saving";
private String powerState = "";
public Laptop(){
setPowerState(Laptop.OFF);
}
public void setPowerState(String powerState){
this.powerState = powerState;
}
public void powerPush(){
if ("on".equals(this.powerState)) {
System.out.println("전원 off");
}
else if ("saving".equals(this.powerState)){
System.out.println("전원 on");
}
else {
System.out.println("절전 모드");
}
}
}
public class Client {
public static void main(String args[]){
Laptop laptop = new Laptop();
laptop.powerPush();
laptop.setPowerState(Laptop.ON);
laptop.powerPush();
laptop.setPowerState(Laptop.SAVING);
laptop.powerPush();
laptop.setPowerState(Laptop.OFF);
laptop.powerPush();
laptop.setPowerState(Laptop.ON);
laptop.powerPush();
}
}
조건문이 하나 추가 된다고 해서 크게 불편한 것은 없는데 뭐가 문제일까요?
그런데 상태가 여러개 있다면 분기하는 코드는 굉장히 길어질 것이기 때문에, 상태에 따라 하고자 하는 행위를 파악하기가 쉽지 않을 것입니다.
또한 Laptop의 powerPush() 메서드 뿐만 아니라, 예를 들면 TV의 powerPush() , SmartPhone의 powerPush() 에서 같은 분기문이 사용된다면, 기능이 변경될 때 마다 일일이 다 찾아가서 수정을 해야 합니다.
따라서 이렇게 상태에 따라 행위를 달리해야 하는 경우에 사용하는 패턴이 스테이트 패턴입니다.
2. 스테이트 패턴 적용
스테이트 패턴을 적용하면 각 상태들, 즉 On, Off, Saving 상태를 클래스로 정의하고, 이들을 하나의 인터페이스로 묶습니다.
그리고나서 Laptop이 상태 인터페이스의 메서드를 호출하면, 각 상태 클래스에서 정의된 행위가 수행되는 방식입니다.
이를 코드로 표현하면 다음과 같습니다.
1)
먼저 전원 상태를 캡슐화한 인터페이스를 선언합니다.
public interface PowerState {
public void powerPush();
}
2)
다음으로 PowerState 인터페이스를 구현한 각 상태 클래스를 정의합니다.
public class On implements PowerState{
public void powerPush(){
System.out.println("전원 off");
}
}
public class Off implements PowerState {
public void powerPush(){
System.out.println("절전 모드");
}
}
public class Saving implements PowerState {
public void powerPush(){
System.out.println("전원 on");
}
}
3)
이어서 Laptop 클래스를 수정합니다.
public class Laptop {
private PowerState powerState;
public Laptop(){
this.powerState = new Off();
}
public void setPowerState(PowerState powerState){
this.powerState = powerState;
}
public void powerPush(){
powerState.powerPush();
}
}
Laptop 클래스는 이제 상태를 분기하는 코드가 사라지고, 인터페이스의 powerPush() 메서드를 호출하기만 합니다.
4)
마지막으로 Laptop 객체를 사용하는 Client 클래스를 정의합니다.
public class Client {
public static void main(String args[]){
Laptop laptop = new Laptop();
On on = new On();
Off off = new Off();
Saving saving = new Saving();
laptop.powerPush();
laptop.setPowerState(on);
laptop.powerPush();
laptop.setPowerState(saving);
laptop.powerPush();
laptop.setPowerState(off);
laptop.powerPush();
laptop.setPowerState(on);
laptop.powerPush();
}
}
이전과 결과는 동일합니다.
이상으로 스테이트 패턴에 대해 알아보았습니다.
스테이트 구현 과정을 보면, 전략 패턴과 상당히 유사합니다.
거의 동일한 구조이죠.
굳이 사용을 구분 하자면, 전략 패턴은 상속을 대체하려는 목적으로, 스테이트 패턴은 코드내의 조건문들을 대체하려는 목적으로 사용됩니다. ( 참고 )
참고로 Java에서도 여러 디자인 패턴을 사용하고 있는데 State 패턴을 사용하고 있는 API는 다음과 같으며, 참고하시면 좋을 것 같습니다. ( 참고링크 )
State
( recognizeable by behavioral methods which changes its behaviour depending on the instance's state which can be controlled externally )
javax.faces.lifecycle.LifeCycle#execute()
(controlled byFacesServlet
, the behaviour is dependent on current phase (state) of JSF lifecycle)