상태 패턴을 위키 백과에선 다음과 같이 정의하고 있다.
상태 패턴(state pattern)은 객체 지향 방식으로 상태 기계를 구현하는 행위, 소프트웨어 디자인 패턴이다. 상태 패턴을 이용하면 상태 패턴 인터페이스의 파생 클래스로서 각각의 상태를 구현함으로써, 또 패턴의 슈퍼클래스에 의해 정의되는 메소드를 호출하여 상태 변화를 구현함으로써 상태 기계를 구현한다.
상태 패턴은 패턴의 인터페이스에 정의된 메소드들의 호출을 통해 현재의 전략을 전환할 수 있는
전략 패턴으로 해석할 수 있다.
쉽게 말해서 여러 조건에 분기 되는 로직을 if 문으로 해결하는 것이 아니라 객체의 다형성을 사용해 해결하는 방법이다. 우아한테크코스 체스 미션을 진행하면서 체스 게임이 진행되는 상태를 상태 패턴으로 구현했다.
체스 미션의 흐름
체스 게임이 진행될 때 사용자 입력을 받아 진행된다.
- 애플리케이션이 실행되고 바로 게임을 시작할 수는 없다.
- "Start" 입력을 받아야 체스 게임이 시작된다.
- "Move" 입력을 통해 말을 움직일 수 있다. 하얀 기물과 검은 기물을 번갈아서 움직여야 한다.
- "End"를 입력 받거나 킹을 잡으면 게임이 종료된다.
게임이 어떤 상태이냐에 따라 실행할 수 있는 입력이 다르고 검증해야 할 내용도 다르다. (하얀 기물을 움직일 차례인데 검은 기물을 움직이는 경우 등) 이럴 때 아무 생각 없이 코드를 짜면 if문이 엄청 많아지고 읽기 어려워질 것이다.
if (game.isReady) { // 게임이 시작되기 전에는
// "Start" 입력만 받을 수 있다.
}
if (game.isRunning) { // 게임이 진행 중일 때는 "move", "end" 입력어만 받을 수 있다.
if (game.isWhiteTurn) { //하얀 기물을 움직일 차례에는
// 검은 기물을 움직이려 하면 예외
// ...
이러면 각 상태에 따른 행동들을 관리하기가 어려울 것이다. 즉 유지보수가 어렵다! 각 상태들을 객체로 만들어서 관리한다면 좋을 것이다.
상태 패턴 사용
게임이 진행되며 변화하는 상태를 객체로 만든 것이다. 최상위에 State 추상 클래스가 있고 각 상태들이 이를 상속하고 있다.
public abstract class GameState {
// ...
public abstract GameState proceed(Command command);
// ...
}
다른 메서드들도 더 있지만 핵심은 proceed()라는 메서드이다. Command 타입을 인수로 받는데 Command 객체에는 사용자 입력이 "start"인지 "move"인지 등의 정보가 담겨있다. GameState를 상속받은 객체는 인수로 받은 Command에 따라 자신들이 받을 수 있는 Command인지를 검증하고 상태가 변화할 필요가 있다면 변화된 상태를 반환할 것이다.
예를 들어 Ready 객체는 다음과 같이 구현되어 있다.
public class Ready extends GameState {
// ...
@Override
public GameState proceed(Command command) {
if (command.isMove()) {
throw new IllegalArgumentException(CANNOT_MOVE);
}
if (command.isStart()) {
return new RunningWhiteTurn(board.getPieces());
}
return new Finished();
}
// ...
}
"Start"가 아닌 "Move" 입력엔 예외를 발생시키고 "Start"를 받으면 하얀 기물을 움직일 차례가 되므로 RunningWhiteTurn을 반환한다. 이 외에는 "End" 입력이므로 Finished를 반환한다.
여기까지만 봐서는 크게 안 와닿을 수도 있다. 이제 이 State를 사용하는 컨트롤러에서 어떤 코드가 쓰여지는지 확인해 보자.
public void run() {
outputView.displayGameRule();
GameState state = new Ready(BoardInitializer.generate());
while (!state.isFinished()) {
state = processOneTurn(state);
}
}
private GameState processOneTurn(GameState state) {
try {
Command command = StringToCommandConverter.from(inputView.askCommand());
state = state.proceed(command);
} catch (IllegalArgumentException | IllegalStateException | NoSuchElementException exception){
outputView.displayErrorMessage(exception);
}
return state;
}
코드를 보면 체스 게임이 준비 상태 -> 하얀 기물을 움직일 차례 -> 검은 기물을 움직일 차례 -> 끝난 상태로의 흐름이 보이지 않고 오직 proceed() 메서드를 State가 Finished 상태가 될 때까지 호출하면서 사용자 입력을 계속 받고 있다. if 문이 하나도 쓰이지 않고 여러 분기를 객체의 다형성만으로 해결하고 있는 것이다.
상태 패턴을 사용해 보고 느낀 장점
새로운 상태(분기)가 추가되더라도 추가가 용이하다. 지저분한 if 문들 사이에서 코드를 추가하는 것이 아니라 해당 상태 객체를 추가한 뒤 State를 상속하여 구현하면 되기 때문이다.
어떤 상태의 로직이 변경되어도 수정이 용이하다. 마찬가지로 지저분한 코드에서 수정하는 것이 아니라 Ready 로직이 수정된다면 Ready 객체만 집중해서 수정하면 된다.
OCP를 만족한다. 상태가 추가, 변경되어도 이를 사용하는 컨트롤러 코드는 건드릴 부분이 거의 없다.
객체의 캡슐화가 잘 되어 자율성이 높아진다. OCP와 이어지는 내용이다. 컨트롤러에서 state = state.proceed(command) 코드를 보면 상태가 커맨드를 받아 자신의 상태를 변화시키는구나 하고 추상적으로 이해할 수 있고, 안에서 어떻게 분기가 진행되는지는 알 필요가 없다.
컨트롤러 코드가 간결하다. 무조건 간결하다고 좋은 건 아니지만 간결해져서 보기가 좋았다.
'JAVA' 카테고리의 다른 글
JVM과 클래스 로더 (0) | 2022.04.12 |
---|---|
테스트 더블(Test Double) (0) | 2022.04.09 |
IllegalArgumentException vs IllegalStateException (0) | 2022.03.31 |
[JAVA] Enum으로 if문 제거하기 (0) | 2022.03.16 |
옵저버 패턴(Observer Pattern) 살펴보기 (0) | 2022.03.05 |