[Design Pattern] 상태 패턴

olwooz·2023년 3월 1일
0

Design Pattern

목록 보기
19/22
객체의 내부 상태가 변경되면 객체의 행동을 바꿀 수 있게 해주는 행동 패턴

유한 상태 기계의 개념과 밀접한 관련이 있음

프로그램이 한 시점에 가질 수 있는 유한한 갯수의 상태 존재

  • 각 상태마다 프로그램은 다르게 행동하고, 한 상태에서 다른 상태로 즉시 변경 가능
  • 현재 상태에 따라 다른 상태로 변경이 될 수도 안 될 수도 있음
  • 이런 변경 규칙을 ‘전이’ 라고 함, 전이 또한 유한하고 미리 정해져 있음

문서 클래스가 초안, 검토, 출판됨이라는 세 가지 상태를 가진다고 가정, ‘출판’ 메서드는 각 상태마다 다르게 작동함

  • 초안 - 문서를 ‘검토’로 변경
  • 검토 - 유저가 관리자일 경우에만 문서를 ‘출판됨’으로 변경
  • 출판됨 - 아무 일도 일어나지 않음

상태 기계들은 대개 객체의 현재 상태에 따라 적절한 행동을 결정하는 많은 조건문들로 구현됨

class Document is
    field state: string
    // ...
    method publish() is
        switch (state)
            "draft":
                state = "moderation"
                break
            "moderation":
                if (currentUser.role == "admin")
                    state = "published"
                break
            "published":
                // Do nothing.
                break
    // ...

조건문 기반 상태 기계의 가장 큰 약점은 상태와 상태에 의존하는 행동들을 클래스에 추가하면 추가할수록 분명해짐 → 전이 로직이 변경되면 모든 메서드의 상태 조건문을 변경해야 할 수 있음

  • 프로젝트가 커질 수록 문제가 커짐

해결책

행동 패턴 - 객체의 가능한 모든 상태들에 대한 새로운 클래스들을 생성하고, 상태별 행동들을 추출해 해당 클래스에 집어넣음

‘컨텍스트’라고 불리는 원본 객체는 모든 행동을 스스로 구현하는 대신, 상태 객체 중 하나에 대한 참조를 저장해 상태와 관련된 작업을 해당 객체에 위임

다른 상태로 객체를 전이시키려면 현재 활성화된 상태 객체를 새 상태 객체로 대체하면 됨

  • 모든 상태 객체들이 같은 인터페이스를 따르고, 컨텍스트가 해당 인터페이스를 통해 객체들과 소통하는 경우에만 가능

전략 패턴과 비슷해 보이지만 한 가지 중요한 차이점 존재

  • 상태 패턴 - 상태들은 서로에 대해 알고 있고, 한 상태에서 다른 상태로의 전이를 시작할 수 있음
  • 전략 패턴 - 거의 서로 알지 못 함

구조

1. 컨텍스트 - concrete 상태 객체 중 하나에 대한 참조를 저장, 해당 객체에 상태 관련 작업을 위임
   - 컨텍스트는 상태 인터페이스를 통해 상태 객체와 소통
   - 컨텍스트는 새로운 상태 객체를 전달받기 위한 setter 노출
    
2. 상태 - 상태별 메서드를 선언하는 인터페이스
   - 메서드들은 모든 concrete 클래스들이 이해 가능해야 함 
   → 몇몇 상태들이 호출되지 않을 메서드들을 가지고 있지 않게 하기 위함
    
3. concrete 상태 - 상태별 메서드에 대한 각자의 구현 제공
   - 여러 상태에서의 비슷한 코드 중복을 막기 위해 공통되는 행동을 캡슐화하는 중간 추상 클래스 제공 가능
    
4. 컨텍스트와 concrete 상태 둘 다 컨텍스트에 연결된 상태 객체를 변경해 
   컨텍스트의 다음 상태를 설정하고 실제 상태 전이를 실행할 수 있음

적용

상태에 따라 다른 행동을 하는 객체가 있고, 상태의 갯수가 많고, 상태별 코드가 자주 바뀌는 경우

- 상태별 코드를 서로 다른 클래스들의 집합으로 추출 
  → 독립적으로 상태를 추가하거나 기존 상태를 변경할 수 있어서 유지보수 비용 절감

클래스 필드 값에 따라 클래스 행동이 달라지게 하는 많은 조건문들이 존재하는 경우

- 조건문들을 대응되는 상태 클래스들의 메서드로 추출 가능
- 상태별 코드와 관련된 임시 필드와 헬퍼 메서드들을 메인 클래스로부터 제거 가능

조건 기반 상태 기계에서 비슷한 상태와 전이끼리 중복된 코드가 많은 경우

- 상태 클래스들의 계층을 구성하고 공통된 코드를 추상 클래스로 추출해 코드 중복 감소

구현방법

1. 어떤 클래스가 컨텍스트의 역할을 할 지 결정
   - 이미 상태에 의존하는 코드를 가지는 기존 클래스가 될 수도 있고, 
     상태별 코드가 여러 클래스에 흩어져 있으면 새로운 클래스가 될 수도 있음
    
2. 상태 인터페이스 선언
   - 컨텍스트에 선언된 모든 메서드를 미러링할 수도 있지만, 상태별 행동을 포함하는 것들만 목표로 설정
    
3. 각 상태별로 상태 인터페이스에서 파생된 클래스 생성 
   → 컨텍스트의 메서드들을 돌며 해당 상태와 연관된 코드를 새로 생성한 클래스에 추출
   - 코드를 상태 클래스로 이동할 때 컨텍스트의 private 멤버들에 의존하는 경우 해결 방법:
      - 필드나 메서드를 public으로 변경
      - 추출할 행동을 컨텍스트의 public 메서드로 만들어 상태 클래스에서 호출
      - 상태 클래스들을 컨텍스트 클래스 내부로 중첩
        
4. 컨텍스트 클래스에 상태 인터페이스 타입의 참조 필드와 
   해당 필드의 값을 overrider할 수 있는 public setter 추가

5. 컨텍스트의 메서드들을 돌며 비어 있는 상태 조건을 해당 상태 객체의 대응되는 메서드 호출로 변경

6. 컨텍스트 상태를 변경하려면 상태 클래스 중 하나의 인스턴스를 생성해 컨텍스트에 전달
   - 컨텍스트, 상태, 클라이언트에서 할 수 있음
   - 완료되면 클래스는 인스턴스화된 concrete 상태 클래스에 의존하게 됨

장단점

장점

- SRP - 특정 상태 관련 코드를 별도 클래스로 분리 가능
- OCP - 기존 상태 클래스들이나 컨텍스트를 변경하지 않고 새 상태 도입 가능
- 거대한 상태 기계 조건문들을 없애 코드 단순화

단점

- 상태가 몇 개 없거나 자주 변하지 않으면 패턴을 적용하는 게 과할 수 있음

다른 패턴과의 관계

- 브리지, 파사드, 전략, 어댑터 - 다른 객체에 작업을 위임하는 합성 기반 패턴이라는 점에서 
  유사하지만 모두 다른 문제를 해결함
  
- 상태 패턴 - 전략 패턴의 확장으로 간주할 수 있음, 두 패턴 모두 합성 기반
  - 전략 패턴 - 객체들은 완전히 독립적이고 서로 인식 못 함
  - 상태 패턴 - concrete 상태 간 의존성에 제한을 두지 않고 스스로 컨텍스트의 상태를 변경할 수 있게 함

TypeScript 예제

/**
 * The Context defines the interface of interest to clients. It also maintains a
 * reference to an instance of a State subclass, which represents the current
 * state of the Context.
 */
class Context {
    /**
     * @type {State} A reference to the current state of the Context.
     */
    private state: State;

    constructor(state: State) {
        this.transitionTo(state);
    }

    /**
     * The Context allows changing the State object at runtime.
     */
    public transitionTo(state: State): void {
        console.log(`Context: Transition to ${(<any>state).constructor.name}.`);
        this.state = state;
        this.state.setContext(this);
    }

    /**
     * The Context delegates part of its behavior to the current State object.
     */
    public request1(): void {
        this.state.handle1();
    }

    public request2(): void {
        this.state.handle2();
    }
}

/**
 * The base State class declares methods that all Concrete State should
 * implement and also provides a backreference to the Context object, associated
 * with the State. This backreference can be used by States to transition the
 * Context to another State.
 */
abstract class State {
    protected context: Context;

    public setContext(context: Context) {
        this.context = context;
    }

    public abstract handle1(): void;

    public abstract handle2(): void;
}

/**
 * Concrete States implement various behaviors, associated with a state of the
 * Context.
 */
class ConcreteStateA extends State {
    public handle1(): void {
        console.log('ConcreteStateA handles request1.');
        console.log('ConcreteStateA wants to change the state of the context.');
        this.context.transitionTo(new ConcreteStateB());
    }

    public handle2(): void {
        console.log('ConcreteStateA handles request2.');
    }
}

class ConcreteStateB extends State {
    public handle1(): void {
        console.log('ConcreteStateB handles request1.');
    }

    public handle2(): void {
        console.log('ConcreteStateB handles request2.');
        console.log('ConcreteStateB wants to change the state of the context.');
        this.context.transitionTo(new ConcreteStateA());
    }
}

/**
 * The client code.
 */
const context = new Context(new ConcreteStateA());
context.request1();
context.request2();
// Output.txt

Context: Transition to ConcreteStateA.
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to ConcreteStateB.
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to ConcreteStateA.

참고 자료: Refactoring.guru

0개의 댓글