상태 패턴을 알아보자.
상태 패턴
상태패턴
내부 상태가 바뀜에 따라 객체의 행동이 바뀔 수 있도록 해준다. 마치 객체의 클래스가 바뀌는 것 같은 결과를 얻을 수 있다.
요구사항
뽑기 기계가 있다고 가정해 보자. 뽑기 기계에는 '동전이 없는 상태(초기 상태)', '동전이 있는 상태', '뽑기가 나온 상태', '뽑기가 매진된 상태'가 있을 것이다. 또한 '동전 투입', 동전 반환', '손잡이 돌림', '알맹이 내보냄'과 같이 4가지 상태를 바꾸는 행동도 있다. 이것을 프로그래밍으로 어떻게 구현해야 할까?
다음과 같이 현재 상태를 저장하는 인스턴스 변수를 만들고 각 상태의 값을 정의한다고 가정해 보자.
// 뽑기 매진 상태
final static int SOLD_OUT = 0;
// 동전이 없는 상태
final static int NO_QUARTER = 1;
// 동전이 들어간 상태
final static int HAS_QUATER = 2;
// 뽑기 판매 상태
final static int SOLD = 3;
4가지 상태에 따라 동전 투입 메서드(insertCoin()), 동전 반환 메서드(ejectQuater()), 손잡이 돌리는 메서드(turnCrank()), 뽑기 내보내는 메서드(produce())를 구현해야 하고 매우 복잡해질 것이다.
insertCoin() 메서드는 다음과 같을 것이다.
// 뽑기 매진 상태
final static int SOLD_OUT = 0;
// 동전이 없는 상태
final static int NO_QUARTER = 1;
// 동전이 들어간 상태
final static int HAS_QUATER = 2;
// 뽑기 판매 상태
final static int SOLD = 3;
private int state = SOLD_OUT;
public void insertCoin() {
if(state == HAS_QUATER) {
System.out.println("You have already inserted coins.");
} else if(state == NO_QUARTER) {
state = HAS_QUATER;
System.out.println("coin inserted");
} else if(state == SOLD_OUT) {
System.out.println("already sold out");
} else if(state == SOLD) {
System.out.println("The product is coming out");
}
}
위 코드처럼 조건문을 통해 상태를 변경하는 방법은 다음 4가지 문제를 고려하지 않고 있다.
- 코드는 OCP를 지키고 있지 않고 있다.
- 상태 전환이 복잡한 조건문 속에 숨어 있어 분명하게 드러나지 않고 있다.
- 바뀌는 부분을 캡슐화하지 않고 있다.
- 새로운 기능을 추가하는 과정이 복잡하며, 기존에 없던 새로운 버그가 생길 가능성이 높다.
이 문제를 해결하기 위해 상태 패턴을 도입해 보자.
상태 패턴 클래스 다이어그램 및 구현
상태패턴이란 내부 상태가 바뀜에 따라 객체의 행동이 바뀔 수 있도록 해준다. 마치 객체의 클래스가 바뀌는 것 같은 결과를 얻을 수 있다. 이 패턴은 객체의 상태를 캡슐화하고, 그 상태에 따른 행동을 각 상태 객체로 옮겨서 관리하는 방식으로 구현된다. 상태 패턴을 사용하면 상태 전환 로직을 상태별 클래스 안으로 캡슐화하여 코드의 유연성과 유지 보수성을 높일 수 있다.
클래스 다이어그램은 다음과 같다.
클래스 다이어그램을 참고하여 코드로 구현하도록 하겠다.
State 코드는 다음과 같다.
package state;
public abstract class State {
public abstract void insertCoin();
public abstract void ejectCoin();
public abstract boolean turncrank();
public abstract void produce();
}
State를 구현한 NoCoinState, OneCoinState, ProduceState, SoldoutState는 다음과 같다.
public class NoCoinState extends State {
private Context context;
public NoCoinState(Context context) {
this.context = context;
}
@Override
public void insertCoin() {
System.out.println("you inserted a coin");
context.setState(context.getOneCoinState());
}
@Override
public void ejectCoin() {
System.out.println("no coin to eject");
}
@Override
public boolean turncrank() {
System.out.println("no coin. please insert a coin");
return false;
}
@Override
public void produce() {
System.out.println("no coin. please insert a coin");
}
}
public class OneCoinState extends State {
private Context context;
public OneCoinState(Context context) {
this.context = context;
}
@Override
public void insertCoin() {
System.out.println("You have already inserted a coin.");
}
@Override
public void ejectCoin() {
System.out.println("eject coin.");
context.setState(context.getNoCoinState());
}
@Override
public boolean turncrank() {
System.out.println("turn crank");
context.setState(context.getProduceState());
return true;
}
@Override
public void produce() {
System.out.println("you can't produce product now");
}
}
public class ProduceState extends State {
Context context;
public ProduceState(Context context) {
this.context = context;
}
@Override
public void insertCoin() {
System.out.println("producing now..");
}
@Override
public void ejectCoin() {
System.out.println("you can't eject a coin. producing now..");
}
@Override
public boolean turncrank() {
System.out.println("producing now..");
return false;
}
@Override
public void produce() {
context.reduceProduceCount();
if(context.getProduceCount() > 0) {
context.setState(context.getNoCoinState());
} else if(context.getProduceCount() == 0) {
context.setState(context.getSoldoutState());
}
}
}
public class SoldOutState extends State {
private Context context;
public SoldOutState(Context context) {
this.context = context;
}
@Override
public void insertCoin() {
System.out.println("sold out");
}
@Override
public void ejectCoin() {
System.out.println("you can't eject coin");
}
@Override
public boolean turncrank() {
System.out.println("sold out");
return false;
}
@Override
public void produce() {
System.out.println("sold out");
}
}
4개의 클래스 모두 각자 상황에 맞춰 State 클래스 안에 있는 메서드를 오버라이드 하고 있다. 또한 공통적으로 Context를 구성하고 생성자에서 인스턴스를 받고 있다. context는 나중에 다른 상태로 전환할 때 필요한 레퍼런스이다.
Context 클래스는 다음과 같다.
public class Context {
private State soldoutState;
private State noCoinState;
private State oneCoinState;
private State produceState;
private State randomPlusCountState;
private State state;
private int produceCount;
public Context(int produceCount) {
this.produceCount = produceCount;
if(produceCount > 0) {
state = noCoinState;
} else {
state = soldoutState;
}
soldoutState = new SoldOutState(this);
noCoinState = new NoCoinState(this);
oneCoinState = new OneCoinState(this);
produceState = new ProduceState(this);
randomPlusCountState = new RandomPlusCountState(this);
}
public void insertCoin() {
state.insertCoin();
}
public void ejectCoin() {
state.ejectCoin();
}
public void turnCrank() {
if(state.turncrank()) {
state.produce();
}
}
public boolean reduceProduceCount() {
if(produceCount <= 0) {
return false;
}
produceCount--;
return true;
}
public State getSoldoutState() {
return soldoutState;
}
public State getNoCoinState() {
return noCoinState;
}
public State getOneCoinState() {
return oneCoinState;
}
public State getProduceState() {
return produceState;
}
public State getRandomPlusCountState() {
return randomPlusCountState;
}
public int getProduceCount() {
return produceCount;
}
public void setState(State state) {
this.state = state;
}
public void setProduceCount(int produceCount) {
this.produceCount = produceCount;
}
}
상태 패턴을 도입함으로써 다음 4가지 효과를 기대할 수 있다.
- 각 상태의 행동을 별개의 클래스로 국지화했다.
- 관리하기 힘든 조건문을 없앴다.
- 각 상태를 변경에는 닫혀있게 하고, State 클래스는 새로운 상태 클래스를 추가하는 확장에는 열려 있도록 했다.(OCP)
- 다이어그램에 가까우면서 이해하기 좋은 클래스 구조를 생산해 냈다.
위에 코드를 보면 구상 상태 클래스(State를 구현한 클래스)에서 다음 상태를 결정하고 있는데 항상 그래야 하는 건 아니다. Context에서 상태 전환 흐름을 결정할 수 있다. 상태 전환이 고정되어 있으면 상태 전환 흐름을 결정하는 코드를 Context에 넣어도 된다. 하지만 상태 전환이 동적으로 결정된다면 상태 클래스 내에 처리하는 것이 좋다. 예를 들어 State에서 NoCoinState 또는 SoldOutState로 전환하는 결정은 실행 중에 남아있는 produceCount에 의해 동적으로 결정될 수밖에 없다.
상태 전환 코드를 상태 클래스에 넣으면 상태 클래스 사이에 의존성이 생기는 단점이 있다. State 구현 코드를 보면 구상 상태 클래스를 코드에 직접 넣는 대신 Context 객체의 Getter 메서드를 써서 의존성을 최소화하려고 노력했다.
상태 패턴 VS 전략 패턴
상태 패턴과 전략 패턴의 클래스 다이어그램을 비교해 보면 비슷하다는 사실을 알게 된다. 상태 패턴을 사용할 때 상태 객체에 일련의 행동이 캡슐화된다. 상황에 따라 Context 객체에서 여러 상태 객체 중 한 객체에게 모든 행동을 맡기게 된다. 그 객체 내부 상태에 따라 현재 상태를 나타내는 객체가 바뀌게 되고, 그 결과로 Context 객체 행동오 바뀌게 된다. 중요한 점은 클라이언트는 상태 객체를 몰라도 된다는 것이다.
전략 패턴은 클라이언트가 Context 객체에게 어떤 전략 객체를 사용할지 지정한다. 전략 패턴은 주로 실행 시에 전략 객체를 변경할 수 있는 유연성을 제공한다. 일반적으로 전략 패턴은 서브클래스를 만드는 방법을 대신해서 유연성을 극대화하는 용도로 쓰인다. 상속을 사용해서 클래스의 행동을 정의하다 보면 행동을 변경할 때 마음대로 변경하기 힘들다. 하지만 전략 패턴을 사용하면 구성으로 행동을 정의하는 객체를 유연하게 바꿀 수 있다.
'디자인패턴' 카테고리의 다른 글
[헤드퍼스트 디자인패턴] 프록시 패턴 (0) | 2024.08.30 |
---|---|
[헤드퍼스트 디자인패턴] 반복자 패턴과 컴포지트 패턴 (0) | 2024.08.23 |
[헤드퍼스트 디자인패턴] 템플릿메서드 패턴 (0) | 2024.08.20 |
[헤드퍼스트 디자인패턴] 커맨드 패턴 (0) | 2024.08.17 |
[헤드퍼스트 디자인패턴] 싱글턴 패턴 (0) | 2024.08.13 |