# 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 observers = new HashSet(); 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는 그것도 처리할 수 있어요. 페드로: 오! 멋지네요.