open:state-pattern

State Pattern

영업 사원 카르멧 깃(Karmen Git)은 시장을 조사한 후, 사용자별 맞춤 기능을 제공하기로 결정했다

페드로: 요구 사항이 어렵지는 않네요

이브: 요구 사항을 명확하게 해 보죠

  • 유료 사용자이면 모든 뉴스를 보여준다.
  • 그렇지 않으면, 최근 10개의 뉴스만을 보여준다
  • 돈을 지불하면 그 금액을 그의 계정 잔액에 더한다
  • 무료 사용자의 잔액이 충분하면 상태를 유료 사용자로 변경한다.

페드로: 상태 패턴이네요! 멋진 패턴이죠. 먼저 사용자의 상태를 나타내는 enum을 만듭니다.

public enum UserState {
  SUBSCRIPTION(Integer.MAX_VALUE),
  NO_SUBSCRIPTION(10);

  private int newsLimit;

  UserState(int newsLimit) {
    this.newsLimit = newsLimit;
  }

  public int getNewsLimit() {
    return newsLimit;
  }
}

페드로: User의 로직은 다음과 같습니다.

public class User {
  private int money = 0;
  private UserState state = UserState.NO_SUBSCRIPTION;
  private final static int SUBSCRIPTION_COST = 30;

  public List<News> newsFeed() {
    return DB.getNews(state.getNewsLimit());
  }

  public void pay(int money) {
    this.money += money;
    if (state == UserState.NO_SUBSCRIPTION
        && this.money >= SUBSCRIPTION_COST) {
      // buy subscription
      state = UserState.SUBSCRIPTION;
      this.money -= SUBSCRIPTION_COST;
    }
  }
}

페드로: 호출해 보죠

User user = new User(); // create default user
user.newsFeed(); // show him top 10 news
user.pay(10); // balance changed, not enough for subs
user.newsFeed(); // still top 10
user.pay(25); // balance enough to apply subscription
user.newsFeed(); // show him all news

이브: 행위(behavior)에 영향을 주는 값을 User 객체 안에 감추었을 뿐이네요. user.newsFeed(subscriptionType)처럼 전략 패턴을 이용해서 값을 직접 전달할 수도 있지요.

페드로: 인정합니다. 상태 패턴은 전략 패턴과 아주 유사하죠. 이 둘은 심지어 UML 다이어ㅓ그램으로 표현할 때도 같은 모양이에요. 하지만 잔액을 캡슐화해서 user 객체 안에 묶어 놓은 것은 다르죠.

이브: 저는 다른 방식을 사용해도 같은 일을 할 수 있다고 생각해요. 그런데 이 방식은, 명시적으로 전략 패턴을 제공하는 대신에, 상태에 의존하는 것이죠. 클로저의 관점에서는 이 패턴을 전략 패턴과 동일한 방법으로 구현할 수 있어요.

페드로: 메소드를 여러 번 호출하면, 객체의 상태가 바뀔 수 있는 데도요?

이브: 맞아요, 하지만 객체의 상태는 전략 패턴과 관련이 없어요. 그것은 단지 구현 상의 한 방법일 뿐이에요.

페드로: 그럼 다른 방식이란 무엇인가요?

이브: 멀티메소드(Multimethods)입니다.

페드로: 멀티 뭐라고요?

이브: 다음 코드를 보세요

(defmulti news-feed :user-state)

(defmethod news-feed :subscription [user]
  (db/news-feed))

(defmethod news-feed :no-subscription [user]
  (take 10 (db/news-feed)))

이브: 다음의 pay함수는 객체의 상태를 바꾼다는 점을 제외하면 평범한 함수일 뿐이에요. 클로저에서는 상태를 가능한 한 최소화하려 하지만, 필요할 때는 사용해야겠지요.

(def user (atom {:name "Jackie Brown"
                 :balance 0
                 :user-state :no-subscription}))

(def ^:const SUBSCRIPTION_COST 30)

(defn pay [user amount]
  (swap! user update-in [:balance] + amount)
  (when (and (>= (:balance @user) SUBSCRIPTION_COST)
             (= :no-subscription (:user-state @user)))
    (swap! user assoc :user-state :subscription)
    (swap! user update-in [:balance] - SUBSCRIPTION_COST)))

(news-feed @user) ;; top 10
(pay user 10)
(news-feed @user) ;; top 10
(pay user 25)
(news-feed @user) ;; all news

페드로: 멀티메소드를 이용한 디스패치(dispatch)가 enum을 이용한 디스패치보다 더 나은가요?

이브: 이 경우에는 그렇지 않지만, 일반적으로는 그렇습니다.

페드로: 설명해 주시겠어요?

이브: 이중 디스패치(double dispatch)라고 들어 보셨나요?

페드로: 잘 므로겠는데요.

이브: 괜찮아요, 그것이 다음에 다룰 방문자 패턴의 주제이거든요


  • open/state-pattern.txt
  • 마지막으로 수정됨: 2021/11/21 09:24
  • 저자 127.0.0.1