TypeScriptIf-Else를 사용하지말고 상태 패턴을 사용해보세요.

Stop Writing If-Else Trees: Use the State Pattern Instead 번역/의역글입니다.

State Design Pattern은 내부 상태가 변경되면 객체가 행동을 바꾸도록하는 행동 소프트웨어 패턴입니다. 더 간단하게 말해서, 상태 패턴은 if/else 혹은 switch문로 작성된 어수선한 코드없이 객체가 현재 상태에 따라서 다르게 동작하게 합니다.


이전 글 "명령형 패턴이 당신 생각보다 유용한 이유"에서 우리는 동장을 객체화하는게 얼마나 코드를 유연하게 만드는지 탐구해봤습니다. 상태 패턴은 비슷한 접근 방식이지만 상태와 그의 맞는 동작을 객체화로 캡슐링하는 것에 더 집중합니다. 명령형 패턴과 함께 상태 패턴은 우리가 긴 조건문 대신 SOLID 디자인 패턴을 따르도록 도와줍니다. - 명령형 패턴과는 다른 문제를 해결하지만요:)


📱 현실 세계에서 비유해보기: 핸드폰 알림 모드

당신이 가진 휴대폰에 있는 다양한 알림 모드를 생각해보세요. 대표적으로 알림모드(Normal), 진동모드(Vibrate), 무소음모드(Silent)가 있습니다.

휴대폰 소유자인 당신은 일을 하거나 미팅중이거나 영화를 볼때 등 상황에 따라 이 모드들을 수동으로 변경합니다. 그리고 당신이 직접 핸드폰의 동작을 설정하지 않아도 상태에 따라 바뀝니다.

이 시나리오는 상태 패턴과 관련이 있습니다.

if-elseenum을 사용하면 안되는 걸까요? 핸드폰의 행동을 단순히 if-elseswitch로 작성할 수 있습니다.

if (mode === 'NORMAL') {
	// ring loud
} else if (mode === 'VIBRATE') {
   // buzz
} else if(mode === 'SILENT') {
  // stay quiet
}

모드가 별로 없다면 이 코드는 괜찮을 겁니다. 하지만 핸드폰에 많은 모드가 있고 그에 따른 여러개에 동작(전화, 문자, 알람, 알림 등등)들이 있다고 생각해보세요. 조건부 분기의 수가 늘어나고, 각 모드에 대한 로직이 코드 전반에 걸쳐 여러 if문에 흩어지게 됩니다. 따라서 유지보수가 어렵고 오류가 생기기 쉬워집니다.

상태 패턴은 State Class를 통해 각 모드에 따른 로직을 분리하면서 더 깔끔한 접근법을 제공합니다. 핸드폰은 상태 객체를 가지고 해당 객체를 통해 동작합니다. 상태가 변경되면 실제로 State 객체를 교체합니다. 이를 통해 복잡한 조건문을 작성하는 대신 다형성을 통해 상태별 알맞은 동작을 처리할 수 있습니다.


💪🏻 상태 패턴이 작동하는 방식

상태 패턴은 몇가지 주요 개념들이 함께 작동합니다.

Context가 어떤 동작을 요청 받으면, 바로 그 요청을 처리하지 않습니다. 대신 작업을 현재 상태 객체에 위임합니다.

context.handleIncomingCall();
// 실제로는 currentState.handleIncomingCall()이 호출됨

각 상태 객체에 따라 동작 방식이 달라지고, 그에 따라 결과도 달라집니다. 이 코드에서는 하나의 메서드 호출이 실제 상태 객체에 따라 다르게 동작함으로써, 다형성이 구현됩니다.

🚫 복잡한 조건문을 지양하세요.

상태 패턴을 이용하는 주요 동기는 코드 전반에 흩어져있는 반복되는 조건부 로직을 제거하는 것입니다. 만약 상태에 따라 객체의 동작이 달라지는 경우, 상태를 추적하기 위해 eunm이나 flags를 사용하고 상태별로 달라지는 동작을 구현하기 위해 메소드 안에 switch/if을 사용할 수 있습니다. 이건 복잡하고 유지보수하기 힘든 코드를 만들 수 있습니다. 상태 패턴은 각 클래스안에 상태별 로직을 구현함으로써 이 문제를 해결합니다.

상태 패턴의 기존 정의에 따르면 객체의 상태에 따라 동작이 달라지는 복잡한 조건문들이 존재할 때, 상태 패턴은 이 조건문의 각 분기를 별도의 클래스로 분리해 상태 자체를 하나의 객체로 취급한다고 합니다. 이러한 캡슐화 덕분에 코드는 OCP(개방-패쇄 원칙)따르게 됩니다. 또한 이는 SRP(단일 책임 원칙)에도 부합합니다.

✅ 이럴때는 상태 패턴을 사용해보세요.

💣 이럴때는 사용하지 않는게 좋아요.


🤨 왜 상태 패턴이 Eumn 혹은 Flag보다 좋을까요?

Eunm 혹은 Flag 은 소프트웨어가 커질 수록 아래와 같은 문제를 가져오게 됩니다.

⚖️ 상태 패턴 장단점

어느 디자인 패턴과 같이 상태 패턴도 장점과 단점이 있습니다.

장점

  1. 깔끔한 코드 정리
  2. 복잡한 조건문 제거
  3. OCP에 친화적인 설계 방식
  4. 상태 변경 로직은 캡슐화
  5. 다형성(Polymorphic)을 활용한 동작

단점

  1. 더 많은 클래스와 복잡도 증가
  2. 상태와 컨텍스트 간의 강한 결합
  3. 높은 러닝커브: 처음 접하는 개발자에게는 코드 흐름을 이해하기 어려울 수 있습니다.
  4. 메모리/성능 오버헤드: 일부 언어에서는 객체를 생성하는 데 약간 더 성능이 필요할 수 있습니다. 하지만 대부분 크게 신경 쓰일 정도는 아닙니다.

단점 완화 하기

클래스가 너무 많아지는 것이 걱정된다면, 일부 언어에서는 상태 객체를 내부 클래스나 익명 클래스로 구현하여 컨텍스트와 함께 묶어둘 수 있습니다. 객체 생성 비용이 걱정된다면, 상태 객체를 매번 새로 만들 필요는 없습니다. 싱글턴이나 익명 클래스를 재사용할 수 있습니다.

예시코드

interface PhoneState {
  handleIncomingCall(): void;
}
 
class NormalState implements PhoneState {
  handleIncomingCall() {
    console.log("벨소리가 울립니다.");
  }
}
 
class SilentState implements PhoneState {
  handleIncomingCall() {
    console.log("아무 반응 없음 (무음)");
  }
}
 
class Phone {
  private state: PhoneState;
 
  constructor(initialState: PhoneState) {
    this.state = initialState;
  }
 
  setState(state: PhoneState) {
    this.state = state;
  }
 
  receiveCall() {
    this.state.handleIncomingCall();
  }
}
 
const phone = new Phone(new NormalState());
phone.receiveCall(); // 벨소리가 울립니다.
phone.setState(new SilentState());
phone.receiveCall(); // 아무 반응 없음 (무음)