open:observer-pattern

Observer Pattern

클로저의 add-watchremove-watch 함수는 참조 타입을 기반으로 옵저버(발행자/구독자) 기능의 기반을 제공한다.

독립 보안 위원회가 해커 다티 헤블(Dartee Hebl)이 그의 계좌에 십억 달러의 잔고를 가지고 있는 것을 포착했다. 그래서 연관 계좌들과의 거래 내역을 추적해야 한다.

페드로: 우리가 셜럭 홈즈가 되는 건가요?

이브: 그런 건 아니지만, 지금 시스템에는 로깅 기능이 없어서, 모든 잔고 변화를 추적할 방법을 찾아야 해요.

페드로: 관찰자들을 추가하면 돼요. 잔고의 변화가 큰 경우에만, 이 사실을 통지하고 그 이유를 추적하면 되죠. 먼저 Observer 인터페이스를 다음과 같이 정의합니다.

public interface Observer {
  void notify(User u);
}

페드로: 그리고 이 인터페이스를 구현하는 관찰자 클래스 두 개를 정의합니다.

class MailObserver implements Observer {
  @Override
  public void notify(User user) {
    MailService.sendToFBI(user);
  }
}

class BlockObserver implements Observer {
  @Override
  public void notify(User u) {
    DB.blockUser(u);
  }
}

페드로: Tracker 클래스에서 이 관찰자 객체들을 관리합니다.

public class Tracker {
  private Set<Observer> observers = new HashSet<Observer>();

  public void add(Observer o) {
    observers.add(o);
  }

  public void update(User u) {
    for (Observer o : observers) {
      o.notify(u);
    }
  }
}

페드로: 그리고 마지막으로 User 객체를 생성할 때 initTracker() 메소드를 호출해 이 두 관찰자 객체를 추가합니다. 그리고 addMoney 메소드에서 만약 거래액이 100$를 넘으면 FBI에 통지하고 이 사용자의 거래를 차단하도록 수정해 줍니다.

public class User {
  String name;
  double balance;
  Tracker tracker;

  public User() {
    initTracker();
  }

  private void initTracker() {
    tracker = new Tracker();
    tracker.add(new MailObserver());
    tracker.add(new BlockObserver());
  }

  public void addMoney(double amount) {
    balance += amount;
    if (amount > 100) {
      tracker.update(this);
    }
  }
}

이브: 왜 관찰자를 따로 두 개 만들었나요? 다음처럼 한 개만 만들어도 될 것 같은 데요.

class MailAndBlock implements Observer {
  @Override
  public void notify(User u) {
    MailService.sendToFBI(u);
    DB.blockUser(u);
  }
}

페드로: 단일 책임 윈칙(Single responsibility principle)을 따른 거죠.

이브: 아, 그렇군요.

페드로: 그러면 관찰자 기능을 동적으로 결합할 수 있게 되거든요.

이브: 예, 알겠어요.

;; Tracker
(def observers (atom #{}))

(defn add [observer]
  (swap! observers conj observer))

(defn notify [user]
  (map #(apply % user) @observers))

;; Fill Observers
(add (fn [u] (mail-service/send-to-fbi u)))
(add (fn [u] (db/block-user u)))

;; User
(defn add-money [user amount]
  (swap! user
         (fn [m]
           (update-in m [:balance] + amount)))
  ;; tracking
  (if (> amount 100) (notify)))

페드로: 거의 같은 방식이네요?

이브: 그래요, 사실 관찰자는 함수를 등록하는 한 가지 방법일 뿐이거든요. 그리고 나서 다른 함수가 그 등록된 함수를 호출하는 거죠.

페드로: 이것도 여전히 패턴이네요.

이브: 물론이죠, 하지만 다음처럼 클로저의 watch 기능을 이용하면 위의 코드를 개선할 수 있어요.

(add-watch
  user
  :money-tracker
  (fn [k r os ns]
    (if (< 100 (- (:balance ns) (:balance os)))
      (notify))))

페드로: 왜 이 방식이 더 나은 거죠?

이브: 우선 add-money 함수가 더 깔끔해졌어요. 단순히 돈을 더하는 일만 하고 있죠. 그리고 watcher는 add-money 함수에서의 변경 내용뿐만 아니라, user에게 일어나는 모든 상태 변화를 추적할 수 있어요.

페드로: 좀 더 설명해 주세요.

이브: 만약 또 다른 함수 secret-add-money가 잔고를 바꾼다 하더라도, watcher는 그것도 처리할 수 있어요.

페드로: 오! 멋지네요.


  • open/observer-pattern.txt
  • 마지막으로 수정됨: 2022/02/20 11:35
  • 저자 127.0.0.1