Observer Pattern
클로저의 add-watch나 remove-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는 그것도 처리할 수 있어요.
페드로: 오! 멋지네요.