[디자인패턴] 스테이트 패턴 (State Pattern)

koline·2023년 9월 11일
0

디자인패턴

목록 보기
21/24

스테이트 패턴


상태패턴이라고도 불리는 스테이트 패턴은 객체 상태를 캡슐화하여 클래스화함으로써 그것을 참조하게 하는 방식으로 상태에 따라 다르게 처리할 수 있도록 행위 내용을 변경하여, 변경 시 원시 코드의 수정을 최소화할 수 있고, 유지보수의 편의성도 갖는 패턴이다.

즉, 객체지향 설계에서 현실의 사물과 대칭되는 유형의 어떤 것이 아니라, 그것의 상태를 클래스화 함으로써 ‘상태의 변화’를 표현하고, 객체 내부 상태 변경에 따라 객체의 행동을 상태에 특화된 행동들로 분리해내며, 다른 행동에 영향을 주지 않고 새로운 행동을 추가할 수 있게 된다.



전략 패턴과의 유사성


전략 패턴(Strategy Pattern)이 '전략 알고리즘'을 클래스로 표현한 패턴이라면, 상태 패턴(State Pattern)은 '객체 상태'를 클래스로 표현한 패턴이라고 보면 된다.

그래서 그런지 상태 패턴의 클래스 다이어그램을 보면 전략 패턴과 매우 유사하다는 점을 볼 수 있다. 왜냐하면 전략 패턴은 전략을 객체화 한거고, 상태 패턴은 상태를 객체화 한것인데 어쨋든 둘다 똑같은 클래스 묶음이기 때문이다.

공통점

  1. 전략 패턴과 상태 패턴은 클래스 다이어그램이 거의 동일하고 코드 사용법도 비슷하다.
  2. 둘다 난잡한 조건 분기를 극복하기 위해 전략 / 상태 형태를 객체화
  3. 둘다 합성(composition)을 통해 상속의 한계를 극복
  4. 둘다 객체의 일련의 행동이 캡슐화되어 객체 지향 원칙을 준수한다.
  5. State는 Strategy의 확장으로 간주될 수도 있다.

차이점

  1. 전략 패턴은 알고리즘을 객체화 하여 클라이언트에서 유연적으로 전략을 제공 / 교체를 한다.
    상태 패턴은 객체의 상태를 객체화하여 클라이언트와 상태 클래스 내부에서 다른 상태로 교체를 한다.
  2. 전략 패턴의 전략 객체는 그 전략만의 알고리즘 동작을 정의 및 수행한다. (만일 전략을 상태화 하면 클래스 폭발이 일어날 수 있다)
    상태 패턴의 상태 객체는 상태가 적용되는 대상 객체가 할수있는 일련의 모든 행동들을 정의 및 수행한다.
  3. 전략 패턴의 전략 객체는 입력값에 따라 전략 형태가 다양하게 될 수 있으니 인스턴스로 구성한다.
    상태 패턴의 상태 객체는 정의된 상태를 서로 스위칭 하기에 메모리 절약을 위해 싱글톤으로 구성한다.



구조


  1. State : 상태를 추상화한 고수준 모듈.
  2. ConcreteState : 구체적인 각각의 상태를 클래스로 표현. State 역할로 결정되는 인터페이스(API)를 구체적으로 구현한다. 다음 상태가 결정되면 Context에 상태 변경을 요청하는 역할도 한다.
  3. Context : State를 이용하는 시스템. 시스템 상태를 나타내는 State 객체를 합성(composition)하여 가지고 있다. 클라이언트로부터 요청받으면 State 객체에 행위 실행을 위임한다.

여기서 상태 클래스는 싱글톤 클래스로 구성한다.
전략 패턴의 전략 객체 같은 경우 매개 값에 따라 알고리즘 수행 형태가 달라질수 있지만, 상태는 그 객체의 현 폼을 나타내는 것이기 때문에 대부분의 상황에서 유일하게 있어야 한다.



구현


// LaptopContext.java (Context)
public class LaptopContext {
    private PowerState powerState;

    public LaptopContext() {
        powerState = new OffState();
    }

    public void printCurrentState() {
        System.out.println(powerState.toString());
    }

    public void pushTypeButton() {
        powerState.pushTypeButton();
    }

    public void pushPowerButton() {
        powerState.pushPowerButton(this);
    }

    public void changeState(PowerState powerState) {
        this.powerState = powerState;
    }

    public void setSavingState() {
        powerState = new SavingState();
        System.out.println("[LaptopContext] putting laptop in saving state...");
    }
}

// PowerState.java (State)
public interface PowerState {
    void pushPowerButton(LaptopContext context);
    void pushTypeButton();
}

// SavingState.java (ConcreteState)
public class SavingState implements PowerState {

    @Override
    public void pushPowerButton(LaptopContext context) {
        System.out.println("[SavingState] turning on laptop...");
        context.changeState(new OnState());
    }

    @Override
    public void pushTypeButton() {
        throw new IllegalStateException("[SavingState] laptop is currently in saving state...");
    }

    public String toString() {
        return "[SavingState] laptop is in saving state now...";
    }    
}

// OnState.java (ConcreteState)
public class OnState implements PowerState {

    @Override
    public void pushPowerButton(LaptopContext context) {
        System.out.println("[OnState] turning laptop off...");
        context.changeState(new OffState());
    }

    @Override
    public void pushTypeButton() {
        System.out.println("[OnState] typing words...");
    }
    
    public String toString() {
        return "[OnState] laptop is on now...";
    }
}

// OffState.java (ConcreteState)
public class OffState implements PowerState {

    @Override
    public void pushPowerButton(LaptopContext context) {
        System.out.println("[OffState] turning laptop on...");
        context.changeState(new OnState());
    }

    @Override
    public void pushTypeButton() {
        throw new IllegalStateException("[OffState] laptop is currently off");
    }
    
    public String toString() {
        return "[OffState] laptop is off now...";
    }
}

// Client.java (Client)
public class Client {
    public static void main(String[] args) {
        LaptopContext laptop = new LaptopContext();

        laptop.printCurrentState();
        
        laptop.pushPowerButton();
        laptop.printCurrentState();
        laptop.pushTypeButton();

        laptop.setSavingState();
        laptop.printCurrentState();
        
        laptop.pushPowerButton();
        laptop.printCurrentState();

        laptop.pushPowerButton();
        laptop.printCurrentState();
    }
}

// 실행 결과
[OffState] laptop is off now...
[OffState] turning laptop on...
[OnState] laptop is on now...
[OnState] typing words...
[LaptopContext] putting laptop in saving state...
[SavingState] laptop is in saving state now...
[SavingState] turning on laptop...
[OnState] laptop is on now...
[OnState] turning laptop off...
[OffState] laptop is off now...

노트북의 켜진 상태, 꺼진 상태, 절전모드 상태를 State 인터페이스를 상속받아 구현한다. Client는 LaptopContext를 이용해 State인터페이스의 구현체인 각 상태들에 접근하며 상태를 변경하는 메소드를 동작시키고 각 상태 클래스들은 다시 LaptopContext객체의 State를 변경시킨다.

위 코드는 상태패턴의 형태를 구현했으나 한가지 문제가 있다. 바로 상태를 변경할 때 마다 객체를 생성한다는 점이다.

사용이 끝난 객체들은 JVM에서 Garbage Collecting을 하지만 이 상태들 자체가 상황마다 특수한 것도 아니고 반복적으로 생성 및 GC를 반복하며 자원을 낭비할 이유가 없다. 그러므로 이를 싱글톤 패턴을 사용하도록 수정해 보겠다.

싱글톤 패턴 적용

// LaptopContext.java (Context)
public class LaptopContext {
    private PowerState powerState;

    public LaptopContext() {
        powerState = OffState.getInstance();
    }

    public void printCurrentState() {
        System.out.println(powerState.toString());
    }

    public void pushTypeButton() {
        powerState.pushTypeButton();
    }

    public void pushPowerButton() {
        powerState.pushPowerButton(this);
    }

    public void changeState(PowerState powerState) {
        this.powerState = powerState;
    }

    public void setSavingState() {
        powerState = SavingState.getInstance();
        System.out.println("[LaptopContext] putting laptop in saving state...");
    }
}

// PowerState.java (State)
public interface PowerState {
    void pushPowerButton(LaptopContext context);
    void pushTypeButton();
}

// SavingState.java (ConcreteState)
public class SavingState implements PowerState {

    private static class SavingStateInstance {
        private static final SavingState INSTANCE = new SavingState();
    }

    public static SavingState getInstance() {
        return SavingStateInstance.INSTANCE;
    }

    @Override
    public void pushPowerButton(LaptopContext context) {
        System.out.println("[SavingState] turning on laptop...");
        context.changeState(OnState.getInstance());
    }

    @Override
    public void pushTypeButton() {
        throw new IllegalStateException("[SavingState] laptop is currently in saving state...");
    }

    public String toString() {
        return "[SavingState] laptop is in saving state now...";
    }    
}

// OnState.java (ConcreteState)
public class OnState implements PowerState {

    private static class OnStateInstance {
        private static final OnState INSTANCE = new OnState();
    }

    public static OnState getInstance() {
        return OnStateInstance.INSTANCE;
    }

    @Override
    public void pushPowerButton(LaptopContext context) {
        System.out.println("[OnState] turning laptop off...");
        context.changeState(OffState.getInstance());
    }

    @Override
    public void pushTypeButton() {
        System.out.println("[OnState] typing words...");
    }
    
    public String toString() {
        return "[OnState] laptop is on now...";
    }
}

// OffState.java (ConcreteState)
public class OffState implements PowerState {

    private static class OffStateInstance {
        private static final OffState INSTANCE = new OffState();
    }

    public static OffState getInstance() {
        return OffStateInstance.INSTANCE;
    }

    @Override
    public void pushPowerButton(LaptopContext context) {
        System.out.println("[OffState] turning laptop on...");
        context.changeState(OnState.getInstance());
    }

    @Override
    public void pushTypeButton() {
        throw new IllegalStateException("[OffState] laptop is currently off");
    }
    
    public String toString() {
        return "[OffState] laptop is off now...";
    }
}

// Client.java (Client)
public class Client {
    public static void main(String[] args) {
        LaptopContext laptop = new LaptopContext();

        laptop.printCurrentState();
        
        laptop.pushPowerButton();
        laptop.printCurrentState();
        laptop.pushTypeButton();

        laptop.setSavingState();
        laptop.printCurrentState();
        
        laptop.pushPowerButton();
        laptop.printCurrentState();

        laptop.pushPowerButton();
        laptop.printCurrentState();
    }
}

// 실행 결과
[OffState] laptop is off now...
[OffState] turning laptop on...
[OnState] laptop is on now...
[OnState] typing words...
[LaptopContext] putting laptop in saving state...
[SavingState] laptop is in saving state now...
[SavingState] turning on laptop...
[OnState] laptop is on now...
[OnState] turning laptop off...
[OffState] laptop is off now...

수정 전 방식과 모든 코드가 동일하지만 각 상태 클래스 내부에 private static final로 객체의 인스턴스를 생상하는 메소드를 전역으로 생성하고 public으로 getInstance 메소드를 열어 줌으로써 상태별로 하나의 인스턴스만 존재하도록 수정하였다.



목적

  1. 객체의 행동(메서드)가 상태(state)에 따라 각기 다른 동작을 할때.
  2. 상태 및 전환에 걸쳐 대규모 조건 분기 코드와 중복 코드가 많을 경우
  3. 조건문의 각 분기를 별도의 클래스에 넣는것이 상태 패턴의 핵심
  4. 런타임단에서 객체의 상태를 유동적으로 변경해야 할때

장점

  1. 상태(State)에 따른 동작을 개별 클래스로 옮겨서 관리 할 수 있다.
  2. 상태(State)와 관련된 모든 동작을 각각의 상태 클래스에 분산시킴으로써, 코드 복잡도를 줄일 수 있다.
  3. 단일 책임 원칙을 준수할 수 있다. (특정 상태와 관련된 코드를 별도의 클래스로 구성)
  4. 개방 폐쇄 원칙을 준수 할 수 있다. (기존 State 클래스나 컨텍스트를 변경하지 않고 새 State를 도입할 수 있다)
  5. 하나의 상태 객체만 사용하여 상태 변경을 하므로 일관성 없는 상태 주입을 방지하는데 도움이 된다.

단점

  1. 상태 별로 클래스를 생성하므로, 관리해야할 클래스 수 증가
  2. 상태 클래스 갯수가 많고 상태 규칙이 자주 변경된다면, Context의 상태 변경 코드가 복잡해지게 될 수 있다.
  3. 객체에 적용할 상태가 몇가지 밖에 없거나 거의 상태 변경이 이루어지지 않는 경우 패턴을 적용하는 것이 과도할 수 있다.



참고


[디자인패턴] 디자인패턴이란? - 생성패턴, 구조패턴, 행위패턴

상태(State) 패턴 - 완벽 마스터하기

profile
개발공부를해보자

0개의 댓글