이 문서는 저자(mishadoff)의 허락을 받아 clojure design patterns를 번역한 것입니다.
오탈자 및 번역 관련 문의는 아래 번역자에게 전자 메일을 보내주시기 바랍니다.
번역자: 구르마, 김영태, 김만명

GoF의 디자인 패턴을 클로저 관점에서 조명하기

고지 사항: 클로저는 동적 타입 언어이자 함수형 프로그래밍 언어여서, 대부분의 패턴은 클로저로 쉽게 구현될 수 있다. 일부 패턴 구현은 부적절하고 이상하게 보일 수도 있지만, 큰 문제는 없다. 이 글에 등장하는 인물은 모두 가공의 인물이다. 특정인과 일치할 수도 있지만, 우연일 뿐이다.

시작

우리가 사용하는 프로그래밍 언어는 개판이다.
그것이 디자인 패턴이 필요한 이유이다.

— Anonymous 익명의 프로그래머

평범한 두 프로그래머 페드로 빌(Pedro Veel)과 이브 도플러(Eve Dopler)가 흔히 부딪히는 소프트웨어 문제를 해결하며 디자인 패턴을 적용하고 있다.

에피소드 1. 커맨드(Command) 패턴

중견 IT 서비스 회사 Serpent Hill & R.E.E는 미국 소비자들을 대상으로 하는 새로운 프로젝트를 수주했다. 첫 번째로 할 일은 새로운 사이트의 회원 가입과 로그인, 로그아웃을 처리하는 것이다.

페드로: 아! 그건 쉬워요. Command 인터페이스면 돼요.

interface Command {
  void execute();
}

페드로: 모든 명령 클래스에서 이 인터페이스를 구현하고 자신만의 execute 메소드를 정의하면 됩니다.

public class LoginCommand implements Command {

  private String user;
  private String password;

  public LoginCommand(String user, String password) {
    this.user = user;
    this.password = password;
  }

  @Override
  public void execute() {
    DB.login(user, password);
  }
}
public class LogoutCommand implements Command {

  private String user;

  public LogoutCommand(String user) {
    this.user = user;
  }

  @Override
  public void execute() {
    DB.logout(user);
  }
}

페드로: 사용법도 간단합니다.

(new LoginCommand("django", "unCh@1ned")).execute();
(new LogoutCommand("django")).execute();

페드로: 어떻게 생각해요, 이브?

이브: 바로 DB.login을 호출하지 않고, 굳이 LoginCommand 안에 감싸는 거죠?

페드로: 여기서 감싸는 것은 중요해요. 그래야 좀 더 다양한 Command 객체를 대상으로 작업을 수행할 수 있거든요.

이브: 왜 그래야 하죠?

페드로: 지연 호출이나, 로깅, 기록 추적, 캐싱 등 쓸 데가 아주 많죠.

이브: 알겠어요. 그럼 이건 어때요?

(defn execute [command]
  (command))

(execute #(db/login "django" "unCh@1ned"))
(execute #(db/logout "django"))

페드로: 이 해시(#) 기호는 도대체 뭐죠?

이브: 다음의 자바 코드의 단축형으로 보면 돼요.

new SomeInterfaceWithOneMethod() {
  @Override
  public void execute() {
    // do
  }
};

페드로: Command 인터페이스처럼 보이네요.

이브: 원한다면, 다음과 같이 해시 기호 없이 구현할 수도 있어요.

(defn execute [command & args]
  (apply command args))

(execute db/login "django" "unCh@1ned")

페드로: 이 경우에 지연 호출을 위해 함수를 저장하려면 어떻게 해야 하나요?

이브: 스스로 답해 보시죠. 함수를 호출하려면 무엇이 필요하죠?

페드로: 함수 이름…​

이브: 그리고?

페드로: …​함수의 인수들.

이브: 맞았어요! 단순히 function-namearguments를 저장해 두었다가 언제든 원할 때 (apply function-name arguments) 형식으로 호출하면 돼요.

페드로: 음…​ 간단해 보이네요.

이브: 물론이죠. Command는 단순히 함수일 뿐이니까요.

단일 메소드 인터페이스는 함수다
역주.

위의 커맨드 패턴 예제에서 Command 인터페이스는 execute() 메소드 하나만 있는 인터페이스였다. 우리는 여기서 메소드가 하나인 인터페이스, 즉 단일 메소드 인터페이스의 역할이 무엇인지 곰곰히 생각해 볼 필요가 있다.

단일 메소드 인터페이스가 하는 일은 정확히 무엇일까? 이 인터페이스를 구현한 클래스의 인스턴스들에 대해서 그 인터페이스의 메소드(이 경우 Command 인터페이스의 execute)를 호출하기 위해서이다. 인스턴스들 즉 객체들은 자바에서는 함수의 파라미터로, 함수의 리턴 값으로, 멤버 변수나 지역 변수 그리고 베열이나 컨테이너 등에 저장할 수 있다. 자바는 객체지향 언어인 것이다.

인터페이스를 구현한 객체들을 전달해서 결국 그 인터페이스의 메소드를 호출하는 것이 목적인 것이다. 하지만 그것이 목적이라면 메소드 자체만을 전달하면 좋지 않을까? 왜 부질없이 객체를 다 전달하는가? 그저 우리는 그 객체의 특정 메소드를 호출하는 것이 관심일 뿐인데 말이다. 객체의 다른 부분들은 필요가 없는 것이다. 우리가 필요한 것은 객체가 아니라 함수다!

하지만 자바에서 함수는 1급이 아니다. 오로지 객체만이 파라미터로 리턴값으로, 변수로, 배열이나 컨테이너의 요소로서의 자격을 지닐 뿐이다. 자바에서는 함수를 그런 식으로 다룰 수는 없다.

만일 함수가 1급이라면 커맨드 패턴은 단지 커맨드 역할을 하는 함수를 전달하기만 하면 된다. 이것이 위의 예제에서 보았듯이 정확히 클로저가 하는 것이다. 클로저를 포함한 모든 함수형 언어에서는 다 마찬가지다. 자바에서는 이것이 불가능하기 때문에, 1급인 객체에 메소드를 메달아 전달한 후, 전달받은 측에서는 객체의 메소드를 불러내어 호출하는 것이다.

결국 단일 메소드 인터페이스는, 함수가 1급이 아니기 때문에, 1급인 객체에 함수를 매달아 전달하기 위한, 어쩔 수 없는 자바의 고육지책인 것이다.

커맨드 패턴 이외의 상당수의 디자인 패턴이 1급 함수를 이용하면 매우 단순하게 구현될 수 있다.

(인터페이스 구현에 있어서의 다형성의 측면이 여기서는 고려되지 않았는데, 이것은 전혀 다른 측면의 문제이기 때문이며, 클로저에서는 멀티 메소드라는 방식으로 자바보다 더 강력한 다형성을 지원한다)

에피소드 2. 전략(Strategy) 패턴

스벤 토리(Sven Tori)는 사용자 목록을 담은 페이지를 보는 데 많은 돈을 쓴다. 그런데 사용자는 이름순으로 정렬되어 있어야 하고, 유료 사용자들은 다른 사용자들보다 앞에 나타나야 한다. 그 이유는 돈을 지불하기 때문이다. 역순으로 정렬할 때에도 유료 사용자들은 앞에 나열되어야 한다.

페드로: 아! 커스텀 비교자(custom comparator)를 제공해서 Collections.sort(users, comparator)를 호출하면 되겠네요.

이브: 커스텀 비교자는 어떻게 구현하시려고요?

페드로: Comparator 인터페이스를 이용해 compare(Object o1, Object o2) 메소드를 구현하면 되요. ReverseComparator 클래스의 경우에도 마찬가지로 Comparator 인터페이스를 구현하면 됩니다.

이브: 말보다는 코드를 보여 주세요!

class SubsComparator implements Comparator<User> {

  @Override
  public int compare(User u1, User u2) {
    if (u1.isSubscription() == u2.isSubscription()) {
      return u1.getName().compareTo(u2.getName());
    } else if (u1.isSubscription()) {
      return -1;
    } else {
      return 1;
    }
  }
}

class ReverseSubsComparator implements Comparator<User> {

  @Override
  public int compare(User u1, User u2) {
    if (u1.isSubscription() == u2.isSubscription()) {
      return u2.getName().compareTo(u1.getName());
    } else if (u1.isSubscription()) {
      return -1;
    } else {
      return 1;
    }
  }
}

Collections.sort(users, new SubsComparator());

Collections.sort(users, new ReverseSubsComparator());

페드로: 클로저로는 어떻게 할 수 있죠?

이브: 예, 다음과 같이 합니다.

(sort (comparator (fn [u1 u2]
                    (cond
                      (= (:subscription u1) (:subscription u2))
                      (neg? (compare (:name u1) (:name u2)))

                      (:subscription u1)
                      true

                      :else
                      false)))
      users)

페드로: 아주 유사하네요.

이브: 하지만 더 간단하게 할 수도 있어요.

;; forward sort
(sort-by (juxt (complement :subscription) :name) users)

;; reverse sort
(sort-by (juxt :subscription :name) #(compare %2 %1) users)

페드로: 세상에! 달랑 한 줄짜리 코드네요.

이브: 보시다시피, 그냥 함수들일 뿐이죠.

페드로: 어쩃거나, 코드를 이해하기는 매우 어렵네요.

이브는 juxtcomplement, sort-by 함수에 대해 설명한다.

10분이 흐른 뒤에

페드로: 전략 패턴 자체를 넘어서는 아주 이상한 방식이네요.

이브: 상관 없어요. 전략 패턴은 단순히 어떤 함수에 인수로 전달되는 함수일 뿐이니까요.

에피소드 3. 상태(State) 패턴

영업 사원 카르멘 깃(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)라고 들어 보셨나요?

페드로: 잘 모르겠는데요.

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

에피소드 4. 방문자(Visitor) 패턴

Natanius S. Selbys suggested to implement functionality which allows users export their messages, activities and achievements in different formats.

나탈리우스 S. 셀비즈(Natanius S. Selbys)는 사용자들이 자신의 메시지와 활동을 다른 포맷으로 내보내는(export) 기능 구현을 제안했다.

이브: 어떻게 하실 생각이세요?

페드로: 항목들(Message, Activity)의 계층도와 파일 포멧들(PDF, XML)의 계층도가 있는 거죠.

abstract class Format { }
class PDF extends Format { }
class XML extends Format { }

public abstract class Item {
  void export(Format f) {
    throw new UnknownFormatException(f);
  }
  abstract void export(PDF pdf);
  abstract void export(XML xml);
}

class Message extends Item {
  @Override
  void export(PDF f) {
    PDFExporter.export(this);
  }

  @Override
  void export(XML xml) {
    XMLExporter.export(this);
  }
}

class Activity extends Item {
  @Override
  void export(PDF pdf) {
    PDFExporter.export(this);
  }

  @Override
  void export(XML xml) {
    XMLExporter.export(this);
  }
}

페드로: 이게 다예요.

이브: 좋아요, 그런데 인수의 타입에 따른 디스패치는 어떻게 하죠?

페드로: 그게 뭐죠?

이브: 다음의 코드를 보시죠.

Item i = new Activity();
Format f = new PDF();
i.export(f);

페드로: 이 코드에는 의심스러운 부분이 없어 보이는데요.

이브: 실제로 이 코드를 실행해 보면 UnknownFormatException이 발생해요.

페드로: 잠깐만요…​ 정말요?

이브: 자바에서는 단일 디스패치만이 가능해요. 다시 말해 i.export(f)를 호출하면, i의 실제 타입에 따라 디스패치 될 뿐 인수인 f는 전혀 고려의 대상이 되지 않아요.

페드로: 놀랍군요. 그러면 인수의 타입에 따른 디스패치는 안된다는 건가요?

이브: 그래서 방문자 패턴이 생긴 것이죠. 먼저 i 타입에 기반해 디스패치한 후, 다시 f.someMethod(i)를 호출해 f의 타입에 기반해서 디스패치하는 방식으로요.

이브: 그런 식의 코드는 어떻게 작성하죠?

이브: 모든 타입에 export 메소드를 일일이 방문자로 정의하면 됩니다.

public interface Visitor {
  void visit(Activity a);
  void visit(Message m);
}

public class PDFVisitor implements Visitor {
  @Override
  public void visit(Activity a) {
    PDFExporter.export(a);
  }

  @Override
  public void visit(Message m) {
    PDFExporter.export(m);
  }
}

이브: 각 아이템은 다른 방문자를 받아들일 수 있도록 인수 형식을 바꾸어 줍니다.

public abstract class Item {
  abstract void accept(Visitor v);
}

class Message extends Item {
  @Override
  void accept(Visitor v) {
    v.visit(this);
  }
}

class Activity extends Item {
  @Override
  void accept(Visitor v) {
    v.visit(this);
  }
}

이브: 그리고 다음과 같은 방식으로 호출합니다.

Item i = new Message();
Visitor v = new PDFVisitor();
i.accept(v);

이브: 모든 것이 제대로 작동합니다. 게다가 Activity와 Message의 코드를 변경하지 않고, 단순히 새로운 방문자를 정의해서 새로운 동작을 추가할 수 있어요.

페드로: 정말로 유용하네요. 하지만 구현은 쉽지 않네요. 클로저에서도 마찬가지인가요?

이브: 그렇지 않아요. 클로저에서는 멀티메소드를 통해 쉽게 구현할 수 있어요.

페드로: 멀티 뭐라고요?

이브: 코드를 보시죠. 먼저 디스패처(dispatcher) 함수를 정의합니다.

(defmulti export
  (fn [item format] [(:type item) format]))

이브: 이 함수는 itemformat을 인수로 받습니다. 예를 들면,

;; Message item
{:type :message :content "Say what again!"}

;; Activity item
{:type :activity :content "Quoting Ezekiel 25:17"}

;; Formats
:pdf, :xml

이브: 그리고 다음과 같이 다양한 조합의 인수를 받는 함수들을 정의합니다. 그러면 디스패처가 어떤 함수를 호출할지 결정합니다.

(defmethod export [:activity :pdf] [item format]
  (exporter/activity->pdf item))

(defmethod export [:activity :xml] [item format]
  (exporter/activity->xml item))

(defmethod export [:message :pdf] [item format]
  (exporter/message->pdf item))

(defmethod export [:message :xml] [item format]
  (exporter/message->xml item))

페드로: 모르는 포맷이 인수로 건네지면 어떻게 하죠?

이브: 다음처럼 디폴트 함수를 지정해 줄 수 있어요.

(defmethod export :default [item format]
  (throw (IllegalArgumentException. "not supported")))

페드로: 좋습니다. 하지만 :pdf:xml 사이에는 아무런 상하 관계(hierarchy)가 존재하지 않네요. 단순히 키워드일 뿐이잖아요?

이브: 맞아요. 단순한 문제여서 해법도 단순해요. 이와 같은 고급 기능이 필요하면 다음과 같이 임의로 계층 관계(hierarchy)를 지정해 줄 수도 있고, class 함수를 디스패처로 사용할 수도 있어요.

(derive ::pdf ::format)
(derive ::xml ::format)

페드로: 콜론이 두 개 있네요!

이브: 일단은 그냥 키워드와 같다고 생각하세요.

페드로: 알겠습니다.

이브: 그리고 ::pdf::xml, ::format에 해당하는 함수들을 다음처럼 추가해 줍니다.

(defmethod export [:activity ::pdf])
(defmethod export [:activity ::xml])
(defmethod export [:activity ::format])

이브: 만약 새로운 포맷 (예를 들면, csv) 처리가 필요하면, 다음과 같이 해 줍니다.

(derive ::csv ::format)

이브: `::csv`를 처리하는 함수를 별도로 제공하지 않으면, ::format을 처리하는 함수가 이를 처리해 줘요.

페드로: 훌륭해 보이네요.

이브: 물론이죠. 게다가 훨씬 쉽지요.

페드로: 그렇다면, 언어가 기본적으로 다중 디스패치를 지원하면, 방문자 패턴은 필요 없다는 건가요?

이브: 맞아요.

에피소드 5. 템플릿 메소드(Template Method) 패턴

MMORPG Mech Dominore Fight Saga requested to implement a game bot for their VIP users. Not fair.

멕 도미노어 파잇 사거(Mech Dominore Fight Saga) MMORPG 게임에서 VIP 사용자들을 위한 게임 봇(bot)을 구현해야 한다.

페드로: 먼저 봇으로 어떤 동작을 자동화해야 할지 결정해야겠어요.

이브: RPG 게임 해본 적 있으세요?

페드로: 다행히, 없어요.

이브: 오, 이런! 가시죠. 보여드릴께요.

2주 후에

페드로: 와우, 제가 +100 공격을 할 수 있는 전설의 검을 찾았어요.

이브: 대단하네요. 하지만 이제 봇을 구현해야 해요.

페드로: 식은 죽 먹기죠. 다음의 상황을 선택하기로 하죠.

  • 전투

  • 임무

  • 상자 열기

페드로: 캐릭터들은 각 상황에서 다르게 행동해요. 예를 들면 마법사(mage)는 전투 상황에서 주문을 겁니다. 하지만 악당(rogue)들은 은밀한 근접전을 선호해요. 잠겨 있는 상자는 대부분의 캐릭터들이 그냥 지나치지만 악당들은 열 수 있어요.

이브: 템플릿 메소드 패턴이 가장 적합한 것 같은데요?

페드로: 그래요. 상위 추상 클래스에서 공통의 알고리즘을 정의하고, 하위 클래스에서 각자의 동작을 구현하는 방식이죠.

public abstract class Character {
  void moveTo(Location loc) {
    if (loc.isQuestAvailable()) {
      Journal.addQuest(loc.getQuest());
    } else if (loc.containsChest()) {
      handleChest(loc.getChest());
    } else if (loc.hasEnemies()) {
      attack(loc.getEnemies());
    }
    moveTo(loc.getNextLocation());
  }

  private void handleChest(Chest chest) {
    if (!chest.isLocked()) {
      chest.open();
    } else {
      handleLockedChest(chest);
    }
  }

  abstract void handleLockedChest(Chest chest);
  abstract void attack(List<Enemy> enemies);
}

페드로: 모든 캐릭터에 공통된 내용은 Character 클래스로 분리했습니다. 이제 하위 클래스들을 만들어, 캐릭터들이 특정 상황에서 어떻게 행동하는지를 정의하면 돼요. 잠겨 있는 상자를 다루는 상황과 적을 공격하는 상황의 행동들을 정의해 보죠.

이브: 마법사 클래스부터 시작하죠.

페드로: 마법사요? 좋습니다. 마법사는 잠긴 상자는 열 수 없어요. 그래서 아무것도 하지 않는 것으로 구현하면 됩니다. 적을 공격할 때는 적의 수가 10명이 넘으면 적들을 움직이지 못하게 하고 공간 이동 주문을 외워 도망칩니다. 10명 이하이면 불덩어리 주문을 외워 공격합니다.

public class MageCharacter extends Character {
  @Override
  void handleLockedChest(Chest chest) {
    // do nothing
  }

  @Override
  void attack(List<Enemy> enemies) {
    if (enemies.size() > 10) {
      castSpell("Freeze Nova");
      castSpell("Teleport");
    } else {
      for (Enemy e : enemies) {
        castSpell("Fireball", e);
      }
    }
  }
}

이브: 훌륭합니다. 그럼 악당 클래스는요?

페드로: 마찬가지로 쉽습니다. 악당들은 상자를 열 수 있고, 은밀한 근접전을 좋아해서 적들을 한 명씩 처리하죠.

public class RogueCharacter extends Character {
  @Override
  void handleLockedChest(Chest chest) {
    chest.unlock();
  }

  @Override
  void attack(List<Enemy> enemies) {
    for (Enemy e : enemies) {
      invisibility();
      attack("backstab", e);
    }
  }
}

이브: 훌륭합니다. 그런데 이 접근법이 전략 패턴과는 어떻게 다르죠?

페드로: 무슨 말씀인지?

이브: 제 말은, 이 패턴에서는 하위 클래스에서 동작을 재정의했는데, 전략 패턴에서도 함수를 이용해 동작을 재정의했었죠.

페드로: 음, 또다른 접근법이라고 할 수 있겠죠.

이브: 상태 패턴에서도 역시 또 다른 방식으로 처리했었죠.

페드로: 무슨 말씀을 하고 싶으신 거죠?

이브: 같은 종류의 문제를 해결하면서 접근하는 방법만 다르다는 것이죠.

페드로: 클로저에서는 전략 패턴을 이용해 이 문제를 어떻게 해결하나요?

이브: 각 캐릭터들의 행동을 정의하는 함수를 그냥 건네주면 돼요. 예를 들면, 추상적인 이동 동작은 대략 다음과 같은 모양일 겁니다:

(defn move-to [character location]
  (cond
    (quest? location)
    (journal/add-quest (:quest location))

    (chest? location)
    (handle-chest (:chest location))

    (enemies? location)
    (attack (:enemies location)))
  (move-to character (:next-location location)))

이브: 각 캐릭터별 handle-chestattack 메소드를 추가하려면, 그 메소드들을 구현한 후 인수로 전달하면 돼요.

;; Mage-specific actions
(defn mage-handle-chest [chest])

(defn mage-attack [enemies]
  (if (> (count enemies) 10)
    (do (cast-spell "Freeze Nova")
        (cast-spell "Teleport"))
    ;; otherwise
    (doseq [e enemies]
      (cast-spell "Fireball" e))))

;; Signature of move-to will change to
(defn move-to [character location
               & {:keys [handle-chest attack]
                  :or {handle-chest (fn [chest])
                       attack (fn [enemies] (run-away))}}]
  (cond
    (quest? location)
    (journal/add-quest (:quest location))

    (chest? location)
    (handle-chest (:chest location))

    (enemies? location)
    (attack (:enemies location)))
  (move-to character (:next-location location)))

페드로: 이런, 이 코드들이 대체 무엇을 하고 있는 거죠?

이브: move-to 함수의 인수가 handle-chestattack 함수를 받아들일 수 있도록 변경한 거에요. 선택 인수(optional parameters)로 생각하면 돼요. 다음과 같이 호출하는 거죠.

(move-to character location
  :handle-chest mage-handle-chest
  :attack       mage-attack)

이브: 이 함수들이 인수로 제공되지 않으면, `handle-chest`의 경우에는 아무런 동작을 하지 않고, attack의 경우에는 적들로부터 도망치는 디폴트 동작을 하도록 정의했어요.

페드로: 좋아요, 하지만 이것이 서브 클래싱보다 더 나은 접근법인가요? move-to 호출시 불필요한 정보를 많이 제공하는 것 같이 보이는데.

이브: 그 점은 개선될 수 있어요. 다음과 같이 하면 간결해져요.

(defn mage-move [character location]
  (move-to character location
    :handle-chest mage-handle-chest
    :attack       mage-attack))

이브: 멀티메소드를 사용하면 더 좋아요.

(defmulti move
  (fn [character location] (:class character)))

(defmethod move :mage [character location]
  (move-to character location
    :handle-chest mage-handle-chest
    :attack       mage-attack))

페드로: 이해했어요. 하지만 인수로 전달하는 것이 서브 클래싱크보다 왜 더 낫다는 거죠?

이브: 동작을 동적으로 변경할 수 있으니까요. 마법사가 에너지가 없다고 가정해 봐요. 그러면 불덩어리들을 던지는 대신에 공간 이동으로 도망칠 수 있어요. 단순히 새로운 함수를 제공하면 돼요.

페드로: 이제 이해가 됩니다. 함수만으로 모든 것이 해결 가능하네요.

에피소드 6. 반복자(Iterator) 패턴

기술 고문 켄트 포디올로리스(Kent Podiololis)가 C 스타일의 반복문 사용에 대해 불평한다.

"우리가 아직도 1980년대에 살고 있는 건가요?"  — 켄트

패드로: 자바의 Iterator 인터페이스를 사용하면 돼요.

이브: 놀리지 말아요. 아무도 java.util.Iterator를 사용하고 있지 않아요.

페드로: 모든 사람이 for-each 루프 구문에서 간접적으로 그것을 사용해요. 그것은 컨테이너를 순회(traverse)하는 좋은 방법이예요.

이브: 컨테이너를 순회한다는 것이 무슨 의미죠?

페드로: 컨테이너는 공식적으로 두 개의 메소드를 제공해야 해요. 즉, next() 메소드는 다음 요소를 반환하고, hasNext() 메소드는 컨테이너가 더 많은 요소를 갖고 있으면 true를 반환해야 하지요.

이브: 좋아요. 그런데 혹시 연결 리스트(linked list)가 무엇인지 아시나요?

페드로: 단일 연결 리스트(Singly linked list) 말씀하시는 건가요?

이브: 맞아요.

페드로: 물론이죠. 그것은 노드들로 구성된 컨테이너죠. 각 노드는 데이터 값과 다음 노드의 레퍼런스를 갖고 있어요. 다음 노드가 없으면 null 값을 갖게 되고요.

이브: 맞아요. 그럼 그런 리스트를 순회하는 것과, 반복자(iterator)를 통해 순회하는 것 사이에 차이가 있을까요?

페드로: 음…​

페드로는 순회하는 코드 두 개를 작성한다.

  • 반복자를 통해 순회하기

Iterator i;
while (i.hasNext()) {
  i.next();
}
  • 연결 리스트를 이용해 순히하기

Node next = root;
while (next != null) {
  next = next.next;
}

페드로: 그러고 보니 둘이 아주 비슷하네요…​ 클로저에서는 반복자에 해당하는 것이 무엇인가요?

이브: seq 함수입니다.

(seq [1 2 3])       => (1 2 3)
(seq (list 4 5 6))  => (4 5 6)
(seq #{7 8 9})      => (7 8 9)
(seq (int-array 3)) => (0 0 0)
(seq "abc")         => (\a \b \c)

페드로: 리스트를 반환하네요…​

이브: 정확히는 시퀀스예요. 반복자는 단순히 시퀀스이거든요.

페드로: 사용자 자료구조도 seq 함수의 인수로 들어갈 수 있나요?

이브: clojure.lang.Seqable 인터페이스를 구현하면 가능해요.

(deftype RedGreenBlackTree [& elems]
  clojure.lang.Seqable
  (seq [self]
    ;; traverse element in needed order
    ))

페드로: 그렇다면 좋네요. 그런데 저는 시퀀스가 지연 평가될 수 있다고 들었어요. 예를 들면 getNext()를 호출할 때에만 값을 계산하게 한다든지 하는 식으로요. 리스트로 어떻게 그것을 처리하나요?

이브: 리스트는 지연 평가될 수 있어요. 클로저에서는 그런 리스트를 "지연 시퀀스"라고 불러요.

(def natural-numbers (iterate inc 1))

이브: 위의 정의로 모든 자연수를 표현할 수 있어요. 하지만 아무런 값도 아직 요청하고 있지 않아서, OutOfMemory가 나지는 않아요.

페드로: 조금 더 설명해 주실 수 있겠어요?

이브: 유감스럽게도, 그러기에는 제가 너무 게으르네요(lazy).

페드로: 알겠습니다!

에피소드 7. 메멘토(Memento) 패턴

사용자 채드 보그(Chad Bogue)가 이틀 동안 작성했던 메시지를 날려 버렸다. 그를 위해 저장 버튼을 구현하라.

페드로: 이틀에 걸쳐 텍스트 박스에 타이핑할 수 있는 사람이 있다니 믿기지가 않네요. 이틀이라니!

이브: 그를 구합시다.

페드로: 이 문제로 구글 검색을 해 보니, 저장 버튼을 구현하는 가장 일반적인 접근법은 메펜토 패턴이네요. originator와 caretaker, memento 객체가 필요해요.

이브: 그것이 다 뭐죠?

페드로: originator는 저장하기 원하는 객체 또는 상태예요. 예를 들면, 텍스트 박스 안의 텍스트를 말해요. caretaker는 상태를 저장하는 일을 맡아요. 예를 들어, 저장 버튼이죠. 그리고 memento는 상태를 보관하는 객체이고요.

public class TextBox {
  // state for memento
  private String text = "";

  // state not handled by memento
  private int width = 100;
  private Color textColor = Color.BLACK;

  public void type(String s) {
    text += s;
  }

  public Memento save() {
    return new Memento(text);
  }

  public void restore(Memento m) {
    this.text = m.getText();
  }

  @Override
  public String toString() {
    return "[" + text + "]";
  }
}

페드로: memento는 불변 객체일 뿐이죠.

public final class Memento {
  private final String text;

  public Memento(String text) {
    this.text = text;
  }

  public String getText() {
    return text;
  }
}

페드로: 그리고 다음의 코드가 caretaker 역할을 합니다.

// open browser, init empty textbox
TextBox textbox = new TextBox();

// type something into it
textbox.type("Dear, Madonna\n");
textbox.type("Let me tell you what ");

// press button save
Memento checkpoint1 = textbox.save();

// type again
textbox.type("song 'Like A Virgin' is about. ");
textbox.type("It's all about a girl...");

// suddenly browser crashed, restart it, reinit textbox
textbox = new TextBox();

// but it's empty! All work is gone!
// not really, you rollback to last checkpoint
textbox.restore(checkpoint1);

페드로: 참고로, 여러 번 저장할 수 있도록 하려면 메멘토들을 리스트에 저장하면 되지요.

이브: originator, caretaker, memento - 필요한 게 너무 많네요. 하지만 실질적으로는 saverestore 함수만 있으면 충분해요.

(def textbox (atom {}))

(defn init-textbox []
 (reset! textbox {:text ""
                  :color :BLACK
                  :width 100}))

(def memento (atom nil))

(defn type-text [text]
  (swap! textbox
    (fn [m]
      (update-in m [:text] (fn [s] (str s text))))))

(defn save []
  (reset! memento (:text @textbox)))

(defn restore []
  (swap! textbox assoc :text @memento))

이브: 그리고 다음은 실행 예고요.

(init-textbox)
(type-text "'Like A Virgin' ")
(type-text "it's not about this sensitive girl ")
(save)
(type-text "who meets nice fella")
;; crash
(init-textbox)
(restore)

페드로: 거의 동일한 코드네요.

이브: 그래요, 하지만 메멘토가 불변값이어야 한다는 것은 주의해야 해요.

페드로: 무슨 의미죠?

이브: 이 예제에서 자바 불변 String 객체를 다룬 것은 운이 좋은 경우예요. 하지만 내부 상태가 변할 수 있는 가변 객체를 다루는 경우에는, 메멘토 객체를 대상으로 깊은 복사(deep copuy)를 수행해 줄 필요가 있거든요.

페드로: 예, 맞아요. 프로토타입을 얻기 위해 재귀적으로 clone()을 호출해 주어야 하죠.

이브: 프로토타입 패턴은 잠시 뒤에 이야기하겠지만, 메멘토 패턴은 저장(save)과 복원(restore)에 관련된 것이지, originator와 caretaker와 관련된 것은 아니라는 것을 기억해 두셔야 해요.

에피소드 8. 프로토타입(Prototype) 패턴

덱스 린지어스(Dex Ringeus)는 사용자들이 회원 등록 양식에 불편함을 느끼는 것을 발견했다. 이것을 좀 더 편리하게 만들어야 한다.

페드로: 등록 양식에 무슨 문제가 있나요?

이브: 사용자들이 입력할 항목들이 너무 많아서 짜증날 정도예요.

페드로: 예를 들면요?

이브: 예를 들면 체중 항목이예요. 여성 사용자들의 90%가 그런 항목을 보면 짜증을 낼 거예요.

페드로: 하지만 이 항목은 우리의 분석 시스템에 중요해요. 이 항목 값에 근거해 음식과 옷을 추천해 주고 있거든요.

이브: 그러면 이 항목을 필수 입력 항목에서 제외하기로 하죠. 이 항목 값이 입력되지 않으면 기본값을 넣어 주고요.

페드로: 60kg이면 적당할까요?

디브: 그런 것 같아요.

페드로: 알았어요. 2분만 기다려 주세요.

두 시간이 흐른 뒤

페드로: 모든 항목이 기본값으로 채워진 등록 양식 프로토타입을 사용해요. 사용자가 양식 작성을 끝냈을 때, 기본값들을 변경하면 돼요.

이브: 좋습니다.

페드로: 여기에 표준 등록 양식이 있어요. clone() 메소드에서는 프로토타입을 사용하고 있고요.

public class RegistrationForm implements Cloneable {
  private String name = "Zed";
  private String email = "zzzed@gmail.com";
  private Date dateOfBirth = new Date(1970, 1, 1);
  private int weight = 60;
  private Gender gender = Gender.MALE;
  private Status status = Status.SINGLE;
  private List<Child> children = Arrays.asList(new Child(Gender.FEMALE));
  private double monthSalary = 1000;
  private List<Brand> favouriteBrands = Arrays.asList("Adidas", "GAP");
  // few hundreds more properties

  @Override
  protected RegistrationForm clone() throws CloneNotSupportedException {
    RegistrationForm prototyped = new RegistrationForm();
      prototyped.name = name;
      prototyped.email = email;
      prototyped.dateOfBirth = (Date)dateOfBirth.clone();
      prototyped.weight = weight;
      prototyped.status = status;
      List<Child> childrenCopy = new ArrayList<Child>();
      for (Child c : children) {
        childrenCopy.add(c.clone());
      }
      prototyped.children = childrenCopy;
      prototyped.monthSalary = monthSalary;
      List<String> brandsCopy = new ArrayList<String>();
      for (String s : favouriteBrands) {
        brandsCopy.add(s);
      }
      prototyped.favouriteBrands = brandsCopy;
    return  prototyped;
  }
}

페드로: 사용자를 만들 때마다, clone()을 호출해서 기본값을 바꿉니다.

이브: 끔직하네요! 가변 자료형의 세상에서는 동일한 값의 객체를 새로 생성하려면 clone()이 필요해요. 난점은 깊은 복사를 해야 한다는 것입니다. 단순히 레퍼런스를 복사하면 안되고, 재귀적으로 내부의 객체들을 clone() 해야만 하지요. 그런데 그 객체들 중의 일부에 clone() 메소드가 없으면 어떻게 될까요?

페드로: 그게 바로 문제인데, 이 패턴은 그 문제를 해결해 주죠.

이브: 제가 보기에, 새로운 객체를 추가해 줄 때마다 clone 메소드를 구현해 주어야만 한다면, 그것은 제대로 된 해결책이라고 보기 힘들다고 생각해요.

페드로: 클로저로는 이런 문제를 어떻게 피할 수 있죠?

이브: 클로저는 불변 자료구조를 제공해요. 그것이 전부예요.

페드로: 불변 자료구조로 프로토타입 문제를 어떻게 해결한다는 거죠?

이브: 객체를 변경할 때마다, 새로운 불변 객체를 얻게 되요. 그래서 예전 객체는 변경되지 않지요. 불변 자료형의 세상에서는 프로토타입 패턴이 필요 없어요.

(def registration-prototype
     {:name          "Zed"
      :email         "zzzed@gmail.com"
      :date-of-birth "1970-01-01"
      :weight        60
      :gender        :male
      :status        :single
      :children      [{:gender :female}]
      :month-salary  1000
      :brands        ["Adidas" "GAP"]})

;; return new object
(assoc registration-prototype
     :name "Mia Vallace"
     :email "tomato@gmail.com"
     :weight 52
     :gender :female
     :month-salary 0)

페드로: 훌륭하네요! 하지만 그런 식으로는 성능에 영향을 미치지 않을까요? 새로운 값을 추가할 때마다 수백만 개의 데이터를 복사하려면 꽤 시간이 걸리지 않나요?

이브: 아니, 그렇지 않아요. 구글에 가서 존속 데이터 구조(persistent data structures)와 구조 공유(structural sharing)에 관해 검색해 보세요.

페드로: 고마워요.

에피소드 9. 중개자(Mediator) 패턴

외부 인사가 최근에 코드를 검토한 결과, 현재의 코드에 문제가 많다고 한다. 특히 비어코 위어드(Veerco Wierde)씨는 채팅 애플리케이션에서의 강한 결합(tight coupling)을 지적하고 있다.

이브: 강한 결합이 뭐죠?

페드로: 한 객체가 다른 객체에 대해 너무 많은 것을 알고 있을 때 나타나는 문제예요.

이브: 좀더 구체적으로 설명해 주실 수 있겠어요?

페드로: 현재의 채팅 프로그램 소스를 보시죠.

public class User {
  private String name;
  List<User> users = new ArrayList<User>();

  public User(String name) {
    this.name = name;
  }

  public void addUser(User u) {
    users.add(u);
  }

  void sendMessage(String message) {
    String text = String.format("%s: %s\n", name, message);
    for (User u : users) {
      u.receive(text);
    }
  }

  private void receive(String message) {
    // process message
  }
}

페드로: 여기서의 문제는 사용자가 모든 다른 사용자들에 관한 정보를 갖고 있다는 것이죠. 이런 코드를 사용하고 유지/보수 하는 것은 대단히 어렵죠. 새로운 사용자가 이 채팅에 들어올 때마다, 모든 사용자가 addUser 메소드를 통해 그 사용자에 대한 레퍼런스를 추가해야만 하거든요.

이브: 그러면 그 일에 해당하는 부분을 다른 클래스로 옮기면 되지 않나요?

페드로: 어느 정도는 맞아요. 모든 사용자들을 관리하는 중개자(mediator)라고 불리는 클래스를 만들면 돼요. 각 사용자는 이 중개자 객체만을 내부에 담게 되죠.

public class User {
  String name;
  private Mediator m;

  public User(String name, Mediator m) {
    this.name = name;
    this.m = m;
  }

  public void sendMessage(String text) {
    m.sendMessage(this, text);
  }

  public void receive(String text) {
    // process message
  }
}

public class Mediator {

  List<User> users = new ArrayList<User>();

  public void addUser(User u) {
    users.add(u);
  }

  public void sendMessage(User u, String text) {
    for (User user : users) {
      u.receive(text);
    }
  }
}

이브: 이것은 단순한 리팩토링 문제같이 보이는 데요.

페드로: 그럴 수도 있지만, 예를 들어, Ui에서 서로 결합된 수백개의 부품들(components)이 있는 경우에는 이 중개자 패턴이 구세주 역할을 할 수 있어요.

이브: 인정합니다.

페드로: 클로저에서는 이런 경우 어떻게 처리하죠?

이브: 음…​ 보아 하니…​ 중개자라는 것이 하는 일이 사용자들을 저장하고 메시지를 보내는 것이네요.

(def mediator
  (atom {:users []
         :send (fn [users text]
                 (map #(receive % text) users))}))

(defn add-user [u]
  (swap! mediator
         (fn [m]
           (update-in m [:users] conj u))))

(defn send-message [u text]
  (let [send-fn (:send @mediator)
        users (:users @mediator)]
    (send-fn users (format "%s: %s\n" (:name u) text))))

(add-user {:name "Mister White"})
(add-user {:name "Mister Pink"})
(send-message {:name "Joe"} "Toby?")

페드로: 아주 좋네요.

이브: 여기에 특별한 것은 없어요. 단지 결합도를 줄이는 방법 중의 하나일 뿐이니까요.

에피소드 10: 관찰자(Observer) 패턴

독립 보안 위원회가 해커 다티 헤블(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)))

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

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

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

Eve Sure, but we can improve solution a bit using clojure watches. 이브: 물론이죠, 하지만 다음처럼 클로저의 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는 그것도 처리할 수 있어요.

페드로: 오! 멋지네요.

에피소드 11. 해석자(Interpreter) 패턴

버티 프레이시(Bertie Prayc)가 우리 서버에서 중요한 데이터를 훔쳐서 비트토렌트(BitTorrent) 시스템을 통해 공유하고 있다. 버티의 가짜 계정을 만들어 그의 평판을 떨어뜨려야 한다.

페드로: 비트토렌트 시스템은 .torrent 파일에 기반하고 있어서 Bencode 인코더를 만들어야 해요.

이브: 네, 그렇다면 먼저 Bencode 포맷에 대해 알아야겠네요.

다음은 Bencode 인코딩 규칙이다.

  • 2개의 데이터 타입이 지원된다.

    • 정수 N은 i<N>e로 인코딩된다. (예) 42 = i42e

    • 문자열 S는 <length>:<contents>로 인코딩된다. (예) hello = 5:hello

  • 2개의 컨데이너가 지원된다.

    • 리스트는 l<contents>e로 인코딩된다. (예) [1, "Bye"] = li1e3:Byee

    • 맵은 d<contents>e로 인코딩된다. (예) {"R" 2, "D" 2} = d1:Ri2e1:Di2ee

      • 키는 단순히 문자열이고, 값에는 모든 Bencode 요소가 허용된다.

페드로: 쉬워 보이네요.

이브: 그럴 수도 있지만, 값들은 중첩될 수 있다는 점을 고려해야 해요. 예를 들면 리스트 안의 리스트 같은.

페드로: 물론이죠. bencode 인코딩에는 해석자 패턴을 사용할 수 있을 것 같네요.

이브: 한번 해 보시죠.

페드로: 모든 bencode 요소들을 위한 인터페이스부터 시작해 보죠.

interface BencodeElement {
  String interpret();
}

페드로: 그리고 나서 각각의 데이터 타입과 데이터 컨데이너에서 위의 인터페이스를 구현합니다.

class IntegerElement implements BencodeElement {
  private int value;

  public IntegerElement(int value) {
    this.value = value;
  }

  @Override
  public String interpret() {
    return "i" + value + "e";
  }
}

class StringElement implements BencodeElement {
  private String value;

  StringElement(String value) {
    this.value = value;
  }

  @Override
  public String interpret() {
    return value.length() + ":" + value;
  }
}

class ListElement implements BencodeElement {
  private List<? extends BencodeElement> list;

  ListElement(List<? extends BencodeElement> list) {
    this.list = list;
  }

  @Override
  public String interpret() {
    String content = "";
    for (BencodeElement e : list) {
      content += e.interpret();
    }
    return "l" + content + "e";
  }
}

class DictionaryElement implements BencodeElement {
  private Map<StringElement, BencodeElement> map;

  DictionaryElement(Map<StringElement, BencodeElement> map) {
    this.map = map;
  }

  @Override
  public String interpret() {
    String content = "";
    for (Map.Entry<StringElement, BencodeElement> kv : map.entrySet()) {
      content += kv.getKey().interpret() + kv.getValue().interpret();
    }
    return "d" + content + "e";
  }
}

페드로: 마지막으로 bencode 인코딩 문자열을 만들 수 있어요.

// discredit user
Map<StringElement, BencodeElement> mainStructure = new HashMap<StringElement, BencodeElement>();
// our victim
mainStructure.put(new StringElement("user"), new StringElement("Bertie"));
// just downloads files
mainStructure.put(new StringElement("number_of_downloaded_torrents"), new IntegerElement(623));
// and nothing uploads
mainStructure.put(new StringElement("number_of_uploaded_torrents"), new IntegerElement(0));
// and nothing donates
mainStructure.put(new StringElement("donation_in_dollars"), new IntegerElement(0));
// prefer dirty categories
mainStructure.put(new StringElement("preffered_categories"),
                      new ListElement(Arrays.asList(
                          new StringElement("porn"),
                          new StringElement("murder"),
                          new StringElement("scala"),
                          new StringElement("pokemons")
                      )));
BencodeElement top = new DictionaryElement(mainStructure);

// let's totally discredit him
String bencodedString = top.interpret();
BitTorrent.send(bencodedString);

이브: 재미있네요, 그런데 코드량이 너무 많네요!

페드로: 기능이 많다 보니 읽기 어려워졌어요.

이브: 코드가 곧 데이터(Code is Data)라는 말을 들어 보신 적 있을 거예요. 그래서 클로저에서는 훨씬 수월해지죠.

;; multimethod to handle bencode structure
(defmulti interpret class)

;; implementation of bencode handler for each type
(defmethod interpret java.lang.Long [n]
  (str "i" n "e"))

(defmethod interpret java.lang.String [s]
  (str (count s) ":" s))

(defmethod interpret clojure.lang.PersistentVector [v]
  (str "l"
       (apply str (map interpret v))
       "e"))

(defmethod interpret clojure.lang.PersistentArrayMap [m]
  (str "d"
       (apply str (map (fn [[k v]]
                         (str (interpret k)
                              (interpret v))) m))
       "e"))

;; usage
(interpret {"user" "Bertie"
            "number_of_downloaded_torrents" 623
            "number_of_uploaded_torrent" 0
            "donation_in_dollars" 0
            "preffered_categories" ["porn"
                                    "murder"
                                    "scala"
                                    "pokemons"]})

이브: 데이터를 정의하는 것이 얼마나 쉬운지 보이세요?

페드로: 그러네요.interpret은 bencode 자료형 당 단순히 하나의 함수네요. 별도의 클래스가 아니라.

이브: 맞아요, 해석자 패턴은 단순히 트리를 처리하는 함수들일 뿐인거죠.

에피소드 12. 플라이웨이트(Flyweight) 패턴

한 법무 법인의 시스템 관리자 크리스토펴 매튼 & 파트(Cristopher, Matton & Pharts)는 보고 시스템이 메모리를 많이 소모해서 가비지 컬렉터가 끊임없이 시스템을 몇 초 동안 멈추게 한다는 것을 발견했다. 그것을 고쳐야 한다.

페드로: 저도 전에 이런 문제를 본 적이 있어요.

이브: 무엇이 잘못된 거죠?

페드로: 많은 점(point) 데이터를 사용하는 실시간 차트 프로그램 때문이예요. 메모리를 많이 소모하거든요. 그래서 가비지 컬렉터가 시스템을 멈추게 하는 거고요.

이브: 음, 그럼 무엇을 해야 하죠?

페드로: 달리 할 수 있는 일이 별로 없어요. 캐싱도 별 도움이 안되고…​

이브:잠깐만요!

페드로: 무슨 일이죠?

이브: 점들은 나이 데이터로 이루어져 있네요. 일반적인 나이(예를 들면, 나이 [0, 100])의 경우, 왜 미리 계산해 놓지 않는 거죠?

페드로: 플라이웨이트 패턴을 사용하라는 말씀인가요?

이브: 제 말은 객체를 재사용하자는 겁니다.

class Point {
  int x;
  int y;

  /* some other properties*/

  // precompute 10000 point values at class loading time
  private static Point[][] CACHED;
  static {
    CACHED = new Point[100][];
    for (int i = 0; i < 100; i++) {
      CACHED[i] = new Point[100];
      for (int j = 0; j < 100; j++) {
        CACHED[i][j] = new Point(i, j);
      }
    }
  }

  Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  static Point makePoint(int x, int y) {
    if (x >= 0 && x < 100 &&
        y >= 0 && y < 100) {
      return CACHED[x][y];
    } else {
      return new Point(x, y);
    }
  }
}

페드로: 이 패턴의 경우에는 두 가지가 필요해요. 프로그램 시작 시점에 대부분의 점 데이터를 미리 계산하는 것과, 생성자 대신에 정적 팩토리 메소드를 이용해서 캐쉬된 객체를 반환하는 것이죠.

이브: 테스트해 보았나요?

페드로: 물론이죠. 시스템은 아주 정확히 동작했습니다.

이브: 좋아요, 다음은 클로저 버전이예요.

(defn make-point [x y]
  [x y {:some "Important Properties"}])

(def CACHE
  (let [cache-keys (for [i (range 100) j (range 100)] [i j])]
      (zipmap cache-keys (map #(apply make-point %) cache-keys))))

(defn make-point-cached [x y]
  (let [result (get CACHE [x y])]
    (if result
      result
      (make-point x y))))

이브: 이차원 배열 대신에, [x y] 좌표를 키로 갖는 일차원 맵을 만듭니다.

페드로: 자바의 경우와 마찬가지네요.

이브; 아니예요, 훨씬 더 유연해요. 세 개의 점이나 정수가 아닌 값을 캐시해야 할 필요가 있을 때에는 이차원 배열을 사용할 수 없으니까요.

페드로: 아, 이해했어요.

이브: 더 좋은 것은, 클로저에서는 memoize 함수를 이용하면 팩토리 함수 make-point를 호출한 결과를 캐싱할 수 있어요.

(def make-point-memoize (memoize make-point))

이브: 그래서 최초의 호출할 때를 제외하고는, 같은 인자를 사용해 함수를 호출하게 되면 캐시에 저장된 값을 반환해요.

페드로: 그것 참 멋진 기능이네요!

이브: 물론이죠. 하지만, 부수 효과(side effects)를 가진 함수인 경우에는 memoize 기능을 사용하지 않는 것이 좋다는 점도 기억해 두세요.

에피소드 13: 빌더(Builder) 패턴

Tuck Brass complains that his old automatic coffee-making system is very slow in usage. Customers can’t wait long enough and going away. 턱 브라스(Tuck Brass)는 자동 커피 메이커 시스템이 오래돼서 너무 느리다고 불평한다. 고객들은 기다리지 못해 그냥 가버린다.

페드로: 무엇이 문제인지 정확히 이해할 필요가 있지 않나요?

이브: 연구해 봤는데, 시스템이 낡았고요, 코볼로 작성된 질의-응답 전문가 시스템으로 구축되어 있어요. 그것은 예전에는 아주 인기가 있었죠.

페드로: "질의-응답"이 무슨 말이에요?

이브: 터미널 앞에 사람이 있어요. 시스템이 "물을 추가할까요?" 라고 물으면, 사람이 "네"라고 답해요. 그러면 시스템이 다시 "커피를 추가할까요?" 라고 물으면, 사람이 "네"라고 답하죠. 뭐 이런 식이죠.

페드로: 악몽이로군요, 난 단지 밀크 커피를 원할 뿐인데요. 왜 미리 준비된 커피를 사용하지 않죠: 밀크 커피, 설탕 커피 등등.

이브: 컴퓨터가 재료를 혼합해서 모든 커피를 만들 수 있기를 원했던 거죠.

페드로: 오케이, 알겠어요. 빌더 패턴으로 고쳐봅시다.

public class Coffee {
  private String coffeeName; // required
  private double amountOfCoffee; // required
  private double water; // required
  private double milk; // optional
  private double sugar; // optional
  private double cinnamon; // optional

  private Coffee() { }

  public static class Builder {
    private String builderCoffeeName;
    private double builderAmountOfCoffee; // required
    private double builderWater; // required
    private double builderMilk; // optional
    private double builderSugar; // optional
    private double builderCinnamon; // optional

    public Builder() { }

    public Builder setCoffeeName(String name) {
      this.builderCoffeeName = name;
      return this;
    }

    public Builder setCoffee(double coffee) {
      this.builderAmountOfCoffee = coffee;
      return this;
    }

    public Builder setWater(double water) {
      this.builderWater = water;
      return this;
    }

    public Builder setMilk(double milk) {
      this.builderMilk = milk;
      return this;
    }

    public Builder setSugar(double sugar) {
      this.builderSugar = sugar;
      return this;
    }

    public Builder setCinnamon(double cinnamon) {
      this.builderCinnamon = cinnamon;
      return this;
    }

    public Coffee make() {
      Coffee c = new Coffee();
        c.coffeeName = builderCoffeeName;
        c.amountOfCoffee = builderAmountOfCoffee;
        c.water = builderWater;
        c.milk = builderMilk;
        c.sugar = builderSugar;
        c.cinnamon = builderCinnamon;

      // check required parameters and invariants
      if (c.coffeeName == null || c.coffeeName.equals("") ||
          c.amountOfCoffee <= 0 || c.water <= 0) {
        throw new IllegalArgumentException("Provide required parameters");
      }

      return c;
    }
  }
}

페드로: 보다시피, 커피 클래스의 인스턴스를 만드는 것이 쉽지 않아요. 내포된 Builder 클래스로 파라미터를 설정할 필요가 있죠.

Coffee c = new Coffee.Builder()
        .setCoffeeName("Royale Coffee")
        .setCoffee(15)
        .setWater(100)
        .setMilk(10)
        .setCinnamon(3)
        .make();

페드로: 메소드를 호출하면 모든 필수 파라미터를 검사를 하는데, 검사하다가 객체의 상태에 뭔가 문제가 있으면 예외를 던지죠.

이브: 멋진 기능이긴 한데, 상당히 장황하네요.

페드로: 클로저로 한 번 해보시죠.

이브: 식은 죽 먹기죠, 클로저는 선택 파라미터를 제공하는데, 그게 빌더 패턴을 대신할 수 있어요.

(defn make-coffee [name amount water
                   & {:keys [milk sugar cinnamon]
                      :or {milk 0 sugar 0 cinnamon 0}}]
  ;; definition goes here
  )

(make-coffee "Royale Coffee" 15 100
             :milk 10
             :cinnamon 3)

페드로: 아하, 파라미터 3개는 필수이고 나머지는 선택 파라미터들이긴 한데, 필수 파라미터에는 이름이 없군요.

이브: 무슨 말이죠?

페드로: 함수 호출할 때 숫자 15를 넘기는데, 그게 무엇을 의미하는지 전혀 모르쟎아요.

이브: 맞네요. 그러면 모든 파라미터에 이름을 짓고, 사전 조건(precondition)을 걸어 보죠. 그럼 당신이 한 것과 똑같죠.

(defn make-coffee
  [& {:keys [name amount water milk sugar cinnamon]
      :or {name "" amount 0 water 0 milk 0 sugar 0 cinnamon 0}}]
  {:pre [(not (empty? name))
         (> amount 0)
         (> water 0)]}
  ;; definition goes here
  )

(make-coffee :name "Royale Coffee"
             :amount 15
             :water 100
             :milk 10
             :cinnamon 3)

이브: 보다시피 모든 파라미터에 이름이 있고, 필수 파라미터는 :pre 조건을 주어 검사되고 있죠. 조건이 위반되면 AssertionError가 발생하죠.

페드로: 재밌네요. :pre는 언어의 일부인가요?

이브: 그렇죠. 단지 간단한 어써션(assertion)이에요. :post 조건도 있는데, 비슷해요.

페드로: 흠, 오케이. 하지만 알다시피 빌더 패턴은 가변 자료구조에 자주 사용되죠. 예를 들어, StringBuilder가 있어요.

이브: 가변 데이타를 사용하는 것은 클로저의 철학과는 맞지 않지만, 굳이 가변 데이타를 사용해야 한다해도, 문제는 없어요. deftype으로 클래스를 만든 다음, 변경하려는 속성에 일시적 가변 변수를 사용하면 돼요.

페드로: 코드를 보여주시죠.

이브: 가변 StringBuilder를 클로저로 구현한 예제가 여기 있어요. 단점도 있고 제한적이지만, 아이디어를 얻을 수 있어요.

;; interface
(defprotocol IStringBuilder
  (append [this s])
  (to-string [this]))

;; implementation
(deftype ClojureStringBuilder [charray ^:volatile-mutable last-pos]
  IStringBuilder
  (append [this s]
    (let [cs (char-array s)]
      (doseq [i (range (count cs))]
        (aset charray (+ last-pos i) (aget cs i))))
    (set! last-pos (+ last-pos (count s))))
  (to-string [this] (apply str (take last-pos charray))))

;; clojure binding
(defn new-string-builder []
  (ClojureStringBuilder. (char-array 100) 0))

;; usage
(def sb (new-string-builder))
(append sb "Toby Wong")
(to-string sb) => "Toby Wong"
(append sb " ")
(append sb "Toby Chung") => "Toby Wang Toby Chung"

페드로: 생각만큼 어렵지는 않네요.

에피소드 14: 파사드(Facade) 패턴

새로운 멤버 유진 라인 주니어(Eugenio Reinn Jr.)가 처리 중인 서블릿에 134개 라인이 추가된 파일을 커밋했다. 하지만 추가된 코드들이 실제로 하는 일은 요청을 처리하는 것 뿐이었다. 즉 클래스들을 임포트해서 사용한 것이 전부이다. 이것은 한 줄짜리 커밋이어야 했다.

페드로: 얼마나 많은 줄이 커밋되었는지 누가 신경이나 쓰나요?

이브: 신경쓰는 사람이 있을 수 있죠.

페드로: 어디가 문제인지 봅시다.

class OldServlet {
  @Autowired
  RequestExtractorService requestExtractorService;
  @Autowired
  RequestValidatorService requestValidatorService;
  @Autowired
  TransformerService transformerService;
  @Autowired
  ResponseBuilderService responseBuilderService;

  public Response service(Request request) {
    RequestRaw rawRequest = requestExtractorService.extract(request);
    RequestRaw validated = requestValidatorService.validate(rawRequest);
    RequestRaw transformed = transformerService.transform(validated);
    Response response = responseBuilderService.buildResponse(transformed);
    return response;
  }
}

이브: 오 이런…​

페드로: 이것은 개발자를 위한 내부 API에요. 요청을 처리할 때마다, 서비스 4개를 써야 하는데, 관련된 모든 임포트를 포함해야 하고, 그래서 이런 코드가 된 거죠.

이브: 리팩토링을 해보죠…​그러니까…​

페드로: …​파사드 패턴. 모든 의존성(dependencies)을 하나의 접점으로 모아서 API를 단순화하죠.

public class FacadeService {
  @Autowired
  RequestExtractorService requestExtractorService;
  @Autowired
  RequestValidatorService requestValidatorService;
  @Autowired
  TransformerService transformerService;
  @Autowired
  ResponseBuilderService responseBuilderService;

  RequestRaw extractRequest(Request req) {
    return requestExtractorService.extract(req);
  }

  RequestRaw validateRequest(RequestRaw raw) {
    return requestValidatorService.validate(raw);
  }

  RequestRaw transformRequest(RequestRaw raw) {
    return transformerService.transform(raw);
  }

  Response buildResponse(RequestRaw raw) {
    return responseBuilderService.buildResponse(raw);
  }
}

페드로: 그래서 어떤 서비스, 혹은 일단의 서비스가 필요하게 되면 단지 파사드만 쓰면 되죠.

class NewServlet {
  @Autowired
  FacadeService facadeService;

  Response service(Request request) {
    RequestRaw rawRequest = facadeService.extractRequest(request);
    RequestRaw validated = facadeService.validateRequest(rawRequest);
    RequestRaw transformed = facadeService.transformRequest(validated);
    Response response = facadeService.buildResponse(transformed);
    return response;
  }
}

이브: 잠깐만요, 방금 모든 의존성을 한 곳으로 옮기고, 매번 그것을 사용한다…​라는 거죠?

페드로:네, 어떤 기능이 필요할 때마다, FacadeService를 사용하죠. 필요한 의존성은 이미 거기에 있으니까요.

이브: 하지만 중개자(Mediator) 패턴에서도 같은 일을 했잖아요?

페드로: 중개자는 행위 패턴이죠. 모든 의존성을 중개자에게 몰아넣고, 거기에 새로운 행위를 추가하죠.

이브: 그러면 파사드는?

페드로: 파사드는 구조 패턴이죠, 파사드 패턴에는 새로운 기능을 추가하지 않아요, 그냥 기존 기능들을 노출할 뿐이죠.

이브: 알겠어요. 하지만 그런 작은 차이에 붙이는 이름 치고는 정말 별나게 거창한 이름이군요.

페드로: 아마도.

이브: 다음 클로저 코드는 이름공간들을 가져와서 사용하고 있어요.

(ns application.old-servlet
  (:require [application.request-extractor :as re])
  (:require [application.request-validator :as rv])
  (:require [application.transformer :as t])
  (:require [application.response-builder :as rb]))

(defn service [request]
  (-> request
      (re/extract)
      (rv/validate)
      (t/transform)
      (rb/build)))

이브: 파사드로 모든 서비스를 노출시키고

(ns application.facade
  (:require [application.request-extractor :as re])
  (:require [application.request-validator :as rv])
  (:require [application.transformer :as t])
  (:require [application.response-builder :as rb]))

(defn request-extract [request]
  (re/extract request))

(defn request-validate [request]
  (rv/validate request))

(defn request-transform [request]
  (t/transform request))

(defn response-build [request]
  (rb/build request))

이브: 그리고 사용하면 되죠.

(ns application.old-servlet
  (:use [application.facade]))

(defn service [request]
  (-> request
      (request-extract)
      (request-validate)
      (request-transform)
      (request-build)))

페드로: :use와 :require의 차이는 뭐죠?

이브: 거의 비슷해요, 하지만 :require는 이름공간을 매번 함수 앞에 붙여줘야 하는 반면, :use는 바로 그럴 필요 없이 바로 사용가능하죠.

페드로: 그러면, :use가 더 좋네요.

이브: 아뇨, :use는 조심해야 해요. 기존 이름공간에서 같은 이름을 사용하면 충돌이 날 수 있죠.

페드로: 오, 무슨 말인지 알겠어요. 어떤 이름공간에서 (:use [application.facade])를 호출할 때마다, 파사드에 있는 함수들 모두가 사용 가능하다?

이브: 네.

페드로: 아주 비슷하네요.

에피소드 15: 싱글톤(Singleton) 패턴

페베로 오닐(Feverro O’Neal)은 UI 스타일이 너무 다양하다고 불평한다. 어플리케이션의 UI 설정에 하나의 스타일만 쓰도록 하자.

페드로: 하지만 잠깐만요, 유저마다 서로 다른 UI 스타일을 저장하라는 요구 사항이 있었죠.

이브: 그 요구사항이 바뀌었어요.

페드로: 오케이, 그러면 설정 사항을 싱글톤에 저장하고 모든 곳에서 싱글톤을 사용하면 돼요.

public final class UIConfiguration {
  public static final UIConfiguration INSTANCE = new UIConfiguration("ui.config");

  private String backgroundStyle;
  private String fontStyle;
  /* other UI properties */

  private UIConfiguration(String configFile) {
    loadConfig(configFile);
  }

  private static void loadConfig(String file) {
    // process file and fill UI properties
    INSTANCE.backgroundStyle = "black";
    INSTANCE.fontStyle = "Arial";
  }

  public String getBackgroundStyle() {
    return backgroundStyle;
  }

  public String getFontStyle() {
    return fontStyle;
  }
}

페드로: 저런 식으로 모든 설정 정보가 UI들 사이에서 공유되는 거죠.

이브: 네, 하지만…​ 코드가 왜 이리 많죠?

페드로: UIConfiguration의 인스턴스가 오직 하나만 존재하도록 보장한거죠.

이브: 물어볼 게 있어요. 싱글톤과 전역변수의 차이가 뭐죠?

페드로: 뭐라구요?

이브: …​싱글톤과 전역 변수의 차이요.

페드로: 자바는 전역변수를 지원하지 않아요.

이브: 하지만 UIConfiguration.INSTANCE는 전역변수에요.

페드로: 글쎄요, 비슷하긴 하죠.

이브: 저 자바 코드는 클로저에서는 그냥 단지 def일 뿐이죠.

(def ui-config (load-config "ui.config"))

(defn load-config [config-file]
  ;; process config file and return map with configuratios
  {:bg-style "black" :font-style "Arial"})

페드로: 하지만, 스타일을 어떻게 바꾸죠?

이브: 자바에서 하는 것과 똑같은 방식으로요.

페드로: 음…​ 오케이, 살짝 바꿔볼게요. UIConfiguration.loadConfig를 public으로 만들고, 설정이 바뀔 때 이 메소드를 호출하는 거죠.

이브: 그러면 ui-config를 아톰으로 하고, 설정이 바뀌면 swap!을 호출하면 돼요.

페드로: 하지만 아톰은 병행 프로그래밍 상황에서만 유용하잖아요.

이브: 맞아요, 유용하죠. 하지만 병행 프로그래밍 상황에만 그런 건 아니에요. 그리고 아톰을 읽는 것은 생각만큼 느리지 않아요. 마지막으로 아톰은 UI 설정을 원자적으로 바꾸죠.

페드로: 저런 단순한 예제에 사용하기에는 아톰이 좀 과분해 보이네요.

이브: 아니요, 그렇지 않아요. 아톰처럼 원자적으로 변경하지 않으면, UI 설정이 바뀔 때 랜더러들이 backgroundStyle는 바뀐 값으로, fontStyle은 이전 값 으로 읽을 가능성이 있어요.

페드로: 그러면, loadConfigsynchronized를 사용하면 돼요.

이브: 그렇게 되면 getter들에도 synchonized를 사용해야 하는데, 그러면 느려지죠.

페드로: Double-Checked Locking 이디엄을 쓰면 돼요.

이브: Double-Checked Locking은 좋은 방법이긴 하지만, 원자성을 완벽하게 보장해 주지는 않죠.[1]

페드로: 오케이, 포기, 당신이 이겼어요.

에피소드 16: 책임 연쇄(Chain Of Responsibility) 패턴

뉴욕의 마케팅 조직인 "A Profit NY"는 그들의 공개 채팅 시스템에서 비속어 필터링을 요청했다.

페드로: 젠장, 그들은 '젠장’이라는 말을 싫어하나?

이브: 수익을 내야 하는 조직이니, 공개 채팅에서 누군가 비속어를 사용하면 수익을 잃겠죠.

페드로: 비속어 리스트는 누가 만들죠?

이브: 조지 칼린이요.[2]

리스트를 보다가 웃으며

페드로: 오케이, 비속어들을 별표로 바꾸는 필터를 추가해 봅시다.

이브: 그 솔루션은 확장성이 있어야 해요, 다른 필터도 적용될 수도 있어야 하거든요.

페드로: 책임 연쇄 패턴은 여기에 딱 맞는 패턴인 것 같네요. 일단 먼저 추상 필터를 만들어 보죠.

public abstract class Filter {
  protected Filter nextFilter;

  abstract void process(String message);

  public void setNextFilter(Filter nextFilter) {
    this.nextFilter = nextFilter;
  }
}

페드로: 그리고 나서 실제로 적용할 필터들 구현해 봅시다.

class LogFilter extends Filter {
  @Override
  void process(String message) {
    Logger.info(message);
    if (nextFilter != null) nextFilter.process(message);
  }
}

class ProfanityFilter extends Filter {
  @Override
  void process(String message) {
    String newMessage = message.replaceAll("fuck", "f*ck");
    if (nextFilter != null) nextFilter.process(newMessage);
  }
}

class RejectFilter extends Filter {
  @Override
  void process(String message) {
    System.out.println("RejectFilter");
    if (message.startsWith("[A PROFIT NY]")) {
      if (nextFilter != null) nextFilter.process(message);
    } else {
      // reject message - do not propagate processing
    }
  }
}

class StatisticsFilter extends Filter {
  @Override
  void process(String message) {
    Statistics.addUsedChars(message.length());
    if (nextFilter != null) nextFilter.process(message);
  }
}

페드로: 마지막으로 메시지가 처리되는 순서를 정의하는 필터의 연쇄를 만듭시다.

Filter rejectFilter = new RejectFilter();
Filter logFilter = new LogFilter();
Filter profanityFilter = new ProfanityFilter();
Filter statsFilter = new StatisticsFilter();

rejectFilter.setNextFilter(logFilter);
logFilter.setNextFilter(profanityFilter);
profanityFilter.setNextFilter(statsFilter);

String message = "[A PROFIT NY] What the fuck?";
rejectFilter.process(message);

이브: 오케이, 이제 클로저로 해보죠. 각 필터는 함수로 정의합니다.

;; define filters

(defn log-filter [message]
  (logger/log message)
  message)

(defn stats-filter [message]
  (stats/add-used-chars (count message))
  message)

(defn profanity-filter [message]
  (clojure.string/replace message "fuck" "f*ck"))

(defn reject-filter [message]
  (if (.startsWith message "[A Profit NY]")
    message))

이브: 그리고 some→ 매크로를 사용해서 필터들을 연결합니다.

(defn chain [message]
  (some-> message
          reject-filter
          log-filter
          stats-filter
          profanity-filter))

이브: 얼마나 쉬운지 아시겠죠? 너무 자연스러워서, 매번 if (nextFilter != null) nextFilter.process() 호출할 필요가 없어요. some→에서 정의한 다음 필터는 setNext를 호출할 필요 없이, 자연스럽게 연결돼요.

페드로: 확실히 조립성(composability)이 더 좋네요. 하지만 왜 가 아닌 some→을 썼죠?

이브: reject-filter 때문이죠. 더 이상 진행할 필요가 없을 수 있는데, 그래서 필터가 nil을 반환하면 some→은 곧바로 nil을 반환하죠.

페드로: 좀 더 설명해 주실 수 있어요?

이브: 사용 예를 보시죠.

(chain "fuck") => nil
(chain "[A Profit NY] fuck") => "f*ck"

페드로: 이해됐어요.

이브: 책임 연쇄 패턴은 단지 함수 합성일 뿐인 거죠.

에피소드 17: 합성(Composite)패턴

여배우인 벨라 호크(Bella Hock)가 소셜 네트워크에서 사용자 아바타를 보지 못하고 있다.

"모든 것이 검은 색이예요. 이거 블랙홀인가요?"

페드로: 이거 검정 사각형이네요.

이브: 흠, 이쪽도 같은 문제가 있어요.

페드로: 마지막에 추가된 기능이 사용자 아바타에 버그를 만든 것 같네요.

이브: 이상하네요, 아바타는 다른 요소들과 같은 방식으로 랜더링해요. 하지만 아바타는 눈에 보이는 거죠.

페드로: 같은 방식으로 랜더링하는 거 확실해요?

이브: 글쎄요…​ 아니요.

코드를 파 본다.

페드로: 도대체 이 코드들이 뭘 하는 거죠?

이브: 누군가 복사해서 붙여넣기를 했네요. 하지만 아바타에 변경 사항을 반영하는 것을 잊었어요.

페드로: 이 코드를 누가 작성했는지 확인해 보죠, git-blame

이브: 확인하는 것도 좋긴 한데, 이 문제를 고칠 필요가 있어요.

페드로: 여기에 한 줄 추가하면 간단하죠.

이브: 제 말은, 진짜 문제를 풀자는 거에요. 같은 블럭들을 처리하는데, 비슷한 코드 2개가 왜 필요하죠?

페드로: 맞네요. 합성 패턴을 사용해서 전체 페이지 랜더링을 처리할 수 있을 것 같아요. 랜더링하는 가장 작은 요소는 블럭이구요.

public interface Block {
  void addBlock(Block b);
  List<Block> getChildren();

  void render();
}

페드로: 당연히 블럭은 다른 블럭을 포함할 수 있어요. 그게 바로 합성 패턴이죠. 구체적인 블럭 몇 개를 만들어 보죠.

public class Page implements Block { }
public class Header implements Block { }
public class Body implements Block { }
public class HeaderTitle implements Block { }
public class UserAvatar implements Block { }

페드로: 그리고 모든 구체적인 요소들을 Block인 것처럼 다룰 수 있죠.

Block page = new Page();
Block header = new Header();
Block body = new Body();
Block title = new HeaderTitle();
Block avatar = new UserAvatar();

page.addBlock(header);
page.addBlock(body);
header.addBlock(title);
header.addBlock(avatar);

page.render();

페드로: 이것은 구조 패턴이에요. 개체들을 합성하는 좋은 방법이죠. 그래서 합성이라고 불리는 거죠.

이브: 저기요. 합성은 단순한 트리 구조네요.

페드로: 그렇죠.

이브: 모든 자료구조를 위한 패턴이 있나요?

페드로: 아니요. 단지 리스트와 트리만을 위한 패턴이 있죠.

이브: 사실, 트리는 리스트로 표현할 수 있어요.

페드로: 어떻게요?

이브: 리스트의 첫 요소는 노드이고요. 그 다음 요소들은 자식들이., 그리고 그들 각각은…​

페드로: 이해했어요.

이브: 좀 더 설명하자면, 다음과 같이 트리가 있어요.

        A
     /  |  \
    B   C   D
    |   |  / \
    E   H J   K
   / \       /|\
  F   G     L M N

이브: 그리고 이 트리를 나타내는 리스트가 다음처럼 돼요.

(def tree
  '(A (B (E (F) (G))) (C (H)) (D (J) (K (L) (M) (N)))))

페드로: 괄호가 많네요!

이브: 괄호는 구조를 만드는 거죠, 알다시피.

페드로: 하지만 파악하기 어려워요.

이브: 기계한테는 쉽죠. 트리를 처리하는 tree-seq라는 멋진 함수가 있어요.

(map first (tree-seq next rest tree)) => (A B E F G C H D J K L M N)

이브: 더 복잡한 순회가 필요하다면, clojure.walk를 사용하면 돼요.

페드로: 모르겠네요, 모든 것이 좀 더 어려워 보이네요.

이브: 아니요, 모든 트리를 자료구조 하나로 정의하고, 그것에 대해 동작하는 하나의 함수만을 사용하는 것이예요.

페드로: 이 함수가 하는 일이 뭐죠?

이브: 트리를 순회하면서 모든 노드에 함수를 적용하는 거죠, 우리의 경우에는 각 컴포넌트들을 랜더링하는 거구요.

페드로: 모르겠네요, 전 트리를 다루기에는 경험이 부족한가 봐요. 다음으로 가죠.

에피소드 18. 팩토리 메소드(Factory Method) 패턴

Sir Dry Bang suggest to create new levels for their popular game. More levels - more money. 드라이 뱅(Dry Bang)씨가 그들의 인기 게임에 새로운 레벨를 만들자고 제안한다. 레벨이 많으면 그만큼 돈도 번다.

페드로: 새 레벨들을 어떻게 만들 수 있나요?

이브: 그저 리소스를 바꾸고 블럭들을 새로 만들고, 종이, 나무, 철…​

페드로: 그건 너무 바보같지 않아요?

이브: 게임 전체가 바보같죠. 유저들이 자신의 캐릭터가 쓸 색깔 모자에 돈을 지불한다면, 나무 블럭에도 지불할 거에요.

페드로: 말도 않되는 거 같지만, 어쨌든, MazeBuilder를 일반적으로 만들고 블럭의 각 타입에 맞는 빌더를 추가합시다. 그건 팩토리 메소드 패턴이죠.

class Maze { }
class WoodMaze extends Maze { }
class IronMaze extends Maze { }

interface MazeBuilder {
  Maze build();
}

class WoodMazeBuilder {
  @Override
  Maze build() {
    return new WoodMaze();
  }
}

class IronMazeBuilder {
  @Override
  Maze build() {
    return new IronMaze();
  }
}

이브: IronMazeBuilderIronMazes를 리턴하는 것은 당연하지 않나요?

페드로: 프로그램한테는 당연한 게 아니죠. 하지만 봅시다, 다른 블럭으로부터 미로를 만들기 위해 우리는 블럭 생성에 책임이 있는 구현체를 단지 변경했어요.

MazeBuilder builder = new WoodMazeBuilder();
Maze maze = builder.build();

이브: 전에 비슷한 것을 봤어요.

페드로: 무엇을요?

이브: 저에게는 이것은 전략 패턴이나 상태 패턴과 비슷해 보여요.

페드로: 말도 안돼요. 전략 패턴은 특정 동작을 수행하는 것에 대한 것이고, 팩토리 패턴은 특정 객체를 만들기 위한 것이에요.

이브: 하지만 생성도 또한 동작이죠.

(defn maze-builder [maze-fn])

(defn make-wood-maze [])
(defn make-iron-maze [])

(def wood-maze-builder (partial maze-builder make-wood-maze))
(def iron-maze-builder (partial maze-builder make-iron-maze))

페드로: 흠, 비슷해 보이네요.

이브: 생각해 보세요.

페드로: 사용 예제 같은 게 있나요?

이브: 아니요, 모든 것이 여기서는 뻔한거라, 그저 전략 패턴이나 상태 패턴, 그리고 템플릿 메소드 패턴 에피소드를 다시 읽으면 돼요.

에피소드 19: 추상 팩토리(Abstract Factory) 패턴

사용자들이 게임의 새 레벨들을 사지 않고 있다. 세이망 게르(Saimank Gerr)가 불평과 관련된 워드클라우드를 만들었는데, 가장 많은 부정적 피드백 단어는 "추해", "형편 없어", "엉망이야" 이다.

레벨 구축 시스템을 개선하라.

페드로: 내가 말했었죠, 이건 형편 없다고.

이브: 확실히 그래요, 눈 배경에도 나무 벽이 있고, 우주 침략자들에도 나무 벽이 있고, 온통 나무 벽이에요.

페드로: 그러니 게임 세계를 레벨 별로 분리하고 각 레벨에 맞는 객체들을 만들어야 해요.

이브: 설명해 주세요.

페드로: 구체적인 블럭을 만드는 데 팩토리 메소드 패턴 대신, 추상 팩토리 패턴을 이용해서 서로 관련된 객체들을 만들어 주면 레벨들이 좀 나아보일 거에요.

이브: 예제가 있으면 좋겠어요.

페드로: 코드가 예제죠. 우선 레벨 팩토리에 추상 행위를 정의해요.

public interface LevelFactory {
  Wall buildWall();
  Back buildBack();
  Enemy buildEnemy();
}

페드로: 그리고 레벨들을 구성하는 객체들의 계층도를 만듭니다.

class Wall {}
class PlasmaWall extends Wall {}
class StoneWall extends Wall {}

class Back {}
class StarsBack extends Back {}
class EarthBack extends Back {}

class Enemy {}
class UFOSoldier extends Enemy {}
class WormScout extends Enemy {}

페드로: 알겠나요? 각 레벨마다 고유한 객체들이 있죠. 이제 팩토리를 만들어 보죠.

class SpaceLevelFactory implements LevelFactory {
  @Override
  public Wall buildWall() {
    return new PlasmaWall();
  }

  @Override
  public Back buildBack() {
    return new StarsBack();
  }

  @Override
  public Enemy buildEnemy() {
    return new UFOSoldier();
  }
}

class UndergroundLevelFactory implements LevelFactory {
  @Override
  public Wall buildWall() {
    return new StoneWall();
  }

  @Override
  public Back buildBack() {
    return new EarthBack();
  }

  @Override
  public Enemy buildEnemy() {
    return new WormScout();
  }
}

페드로: 각 레벨 팩토리의 클래스들은 자신의 레벨과 관련된 객체들을 만들어요. 레벨들은 이제 확실히 더 나아졌어요.

이브: 어디 봅시다. 그런데 어떤 차이가 있는지 모르겠네요.

페드로: 팩토리 메소드 패턴은 객체 생성을 하위 클래스로 넘겨요. 추상 팩토리 패턴은 같은 일을 하지만 서로 관련된 객체들에 대해서 그렇게 하죠.

이브: 아하, 추상 빌더에 서로 관련된 함수들을 전달하면 된다는 의미로군요.

(defn level-factory [wall-fn back-fn enemy-fn])

(defn make-stone-wall [])
(defn make-plasma-wall [])

(defn make-earth-back [])
(defn make-stars-back [])

(defn make-worm-scout [])
(defn make-ufo-soldier [])

(def underground-level-factory
  (partial level-factory
           make-stone-wall
           make-earth-back
           make-worm-scout))

(def space-level-factory
  (partial level-factory
           make-plasma-wall
           make-stars-back
           make-ufo-soldier))

페드로: 알겠어요.

이브: 모든 것이 깔끔하죠. 당신이 좋아하는 "서로 관련된 X들의 집합", 여기서 X는 함수죠.

페드로: 네, 그런데 partial은 뭐죠?

이브: 함수에 파라미터를 일부만 주는 거에요. 그래서 underground-level-factory는 wall과 back과 enemy를 구축하는 법을 알죠. 나머지 작업은 level-factory 추상 함수가 제공하죠.

페드로: 편리하네요.

에피소드 20: 어댑터(Adapter) 패턴

딤 이블(Deam Evil)은 기사의 결투 시합 게임을 한다. 상금은 10만 달러이다.

"만약 당신들이 시스템을 깨고 나의 중무장한 코만도를 이 시합에 넣어 주면 상금의 반을 주겠소."

페드로: 드디어 재밌는 일을 하게 되네요.

이브: 시합을 보는 것은 재밌죠. 특히 M16과 철검의 대결이라면…​

페드로: 기사들은 좋은 갑옷을 입고 있어요.

이브: F1 수류탄한테는 갑옷은 소용 없어요.

페드로: 신경쓸 거 없어요, 우리는 일을 하고, 돈을 받으면 돼요.

이브: 5만 달러 - 괜찮은 액수네요.

페드로: 네, 이것을 보세요, 시합 시스템의 소스를 훔쳤어요. 소스를 고칠 수는 없지만, 약점을 찾을 수 있어요.

이브: 여기 있어요.

public interface Tournament {
  void accept(Knight knight);
}

페드로: 아하! 시스템은 Knight 인터페이스로 들어오는 입력 타입만을 검사하고 있어요. 우리는 그저 commando를 기사로 맞추어 넣기(adapt)만 하면 돼요. 기사가 어떤 모양인지 봅시다.

interface Knight {
  void attackWithSword();
  void attackWithBow();
  void blockWithShield();
}

class Galahad implements Knight {
  @Override
  public void blockWithShield() {
    winkToQueen();
    take(shield);
    block();
  }

  @Override
  public void attackWithBow() {
    winkToQueen();
    take(bow);
    attack();
  }

  @Override
  public void attackWithSword() {
    winkToQueen();
    take(sword);
    attack();
  }
}

페드로: 코만도를 받기 위해서 이전 구현 코드를 가져옵시다.

class Commando {
    void throwGrenade(String grenade) { }
    shot(String rifleType) { }
}

:     .

class Commando implements Knight {
  @Override
  public void blockWithShield() {
    // commando don't block
  }

  @Override
  public void attackWithBow() {
    throwGrenade("F1");
  }

  @Override
  public void attackWithSword() {
    shotWithRifle("M16");
  }
}

페드로: 됐어요.

이브: 클로저로는 더 쉬워요.

페드로: 정말로요?

이브: 클로저는 타입을 쓰지 않아서 타입 검사는 전혀 동작하지 않죠.

페드로: 그러면 기사를 어떻게 코만도로 바꾸죠?

이브: 기본적으로 기사란 무엇이죠? 그것은 맵이죠, 데이타와 행위로 된.

{:name "Lancelot"
 :speed 1.0
 :attack-bow-fn attack-with-bow
 :attack-sword-fn attack-with-sword
 :block-fn block-with-shield}

이브: 코만도를 기사로 맞추기 위해서는 단지 원래의 것 대신 코만도의 함수를 전달하면 되요.

{:name "Commando"
 :speed 5.0
 :attack-bow-fn (partial throw-grenade "F1")
 :attack-sword-fn (partial shot "M16")
 :block-fn nil}

페드로: 우리는 돈을 어떻게 나눌까요?

이브: 50 대 50

페드로: 내가 더 많은 코드를 작성했으니, 전 70을 원해요.

이브: 오케이, 70 대 70

페드로: 좋아요.

에피소드 21: 데코레이터(Decorator) 패턴

포드래 베스퍼(Podrea Vesper)는 시합에서 우리가 속임수를 사용한 것을 적발했다. 우리에게는 하나의 선택권이 있다: 경찰에 체포되거나 그의 슈퍼 기사를 시합에 넣어주는 것이다.

페드로: 난 감옥에 가고 싶지 않아요.

이브: 저도요.

페드로: 그를 위해 한번 더 해보죠.

이브: 같은 방식이죠?

페드로: 비슷하지만, 똑같지는 않아요. 코만도는 군인이어서 시합에 참여하는 것이 허용되지 않죠. 그래서 우리가 맞추어 넣은 거구요. 하지만 기사는 시합 참여가 가능하니, 맞출 필요가 없구요. 기존 것에 기능을 추가하면 되요.

이브: 상속이나 합성?

페드로: 합성, 런타임에 행위를 바꾸는 데코레이터 패턴의 주요 목적이죠.

이브: 그러면 이 슈퍼 기사를 어떻게 할까요?

페드로: 그들은 Galahad 기사를 사용해서 더 많은 HP와 강력 갑옷을 장식할 계획이에요.

이브: 헤, 경찰이 게임을 하다니 재밌네요.

페드로: 네, 기사를 추상 클래스로 만듭시다.

public class Knight {
    protected int hp;
    private Knight decorated;

    public Knight() { }

    public Knight(Knight decorated) {
        this.decorated = decorated;
    }

    public void attackWithSword() {
        if (decorated != null) decorated.attackWithSword();
    }

    public void attackWithBow() {
        if (decorated != null) decorated.attackWithBow();
    }

    public void blockWithShield() {
        if (decorated != null) decorated.blockWithShield();
    }
}

이브: 그러면 저기서 무엇을 개선해야 되죠?

페드로: 인터페이스 대신에 클래스 Knight를 만들어서 hp(hit point) 멤버변수에 접근해요. 그러고 나서 2개의 생성자를 만드는데, 하나는 표준 행위를 위한 디폴트이고, 다른 하나는 데코레이트된 객체에 호출을 위임하는 데코레이트된 생성자이죠.

이브: 인터페이스 대신 추상 클래스를 쓰는 게 맞아요?

페드로: 아니요, 하지만 비슷한 행위를 하는 2개의 클래스를 피하고, 각 데코레이트된 객체를 기본 구현으로 채우는 거죠. 각 메소드의 구현을 강제하는 대신.

이브: 오케이, 특수 갑옷은요?

페드로: 역시 쉬워요.

public class KnightWithPowerArmor extends Knight {
    public KnightWithPowerArmor(Knight decorated) {
        super(decorated);
    }

    @Override
    public void blockWithShield() {
        super.blockWithShield();
        Armor armor = new PowerArmor();
        armor.block();
    }
}

public class KnightWithAdditionalHP extends Knight {
    public KnightWithAdditionalHP(Knight decorated) {
        super(decorated);
        this.hp += 50;
    }
}

페드로: 경찰의 요구 사항을 충족하는 2개의 데코레이터에요. 이제 슈퍼 기사를 만들 수 있어요. 이 슈퍼 기사는 Galahad와 같은 동작을 하지만, 특수 갑옷을 입고 있고, hp 점수도 50점 더 높아요.

Knight superKnight =
     new KnightWithAdditionalHP(
     new KnightWithPowerArmor(
     new Galahad()));

이브: 괜찮은 트릭이네요.

페드로: 자 이제 클로저로 비슷한 행위를 보여줘 보시죠.

이브: 여기요.

(def galahad {:name "Galahad"
              :speed 1.0
              :hp 100
              :attack-bow-fn attack-with-bow
              :attack-sword-fn attack-with-sword
              :block-fn block-with-shield})

(defn make-knight-with-more-hp [knight]
  (update-in knight [:hp] + 50))

(defn make-knight-with-power-armor [knight]
  (update-in knight [:block-fn]
             (fn [block-fn]
               (fn []
                 (block-fn)
                 (block-with-power-armor)))))

;; create the knight
(def superknight (-> galahad
                     make-knight-with-power-armor
                     make-knight-with-more-hp)

페드로: 똑같은 기능이네요.

이브: 네, 다만 특수 갑옷 데코레이터(make-knight-with-power-armor)만 주의해서 보세요.

에피소드 22: 프록시(Proxy) 패턴

데렌 바트(Deren Bart)는 칵테일 만드는 시스템을 관리하고 있다. 이 시스템은 불편했는데, 칵테일을 만든 후 바트는 직접 손으로 쉐이커에서 남은 재료를 빼내야 했기 때문이다. 이것을 자동화하라.

페드로: 바트의 코드 베이스에 접근할 수 있나요?

이브: 아니요, 하지만 바트가 API를 몇 개 보내줬어요.

interface IBar {
    void makeDrink(Drink drink);
}

interface Drink {
    List<Ingredient> getIngredients();
}

interface Ingredient {
    String getName();
    double getAmount();
}

페드로: 바트는 소스에 손 대는 것을 원치 않아요, 대신에 IBar 인터페이스를 구현한 클래스를 추가로 몇 개 만들어서 남은 재료를 자동으로 제거할 필요가 있어요.

이브: 그러면 우리가 맡아야 할 일은 무엇인가요?

페드로: 프록시 패턴을 구현하죠. 얼마 전에 그것에 대해 읽었어요.

이브: 어서 말해 봐요.

페드로: 기본적으로 기존 모든 기능은 IBar를 구현한 표준(standard) 클래스에 위임하고, 새 기능은 ProxiedBar에 넣는 것이죠.

class ProxiedBar implements IBar {
    BarDatabase bar;
    IBar standardBar;

    public void makeDrink(Drink drink) {
       standardBar.makeDrink(drink);
       for (Ingredient i : drink.getIngredients()) {
           bar.subtract(i);
       }
    }
}

페드로: StandardBar 구현 클래스를 우리의 ProxiedBar로 바꿔야 해요.

이브: 아주 간단해 보이네요.

페드로: 네, 게다가 덤으로 우리가 기존 기능을 깨지 않아도 돼요.

이브: 확실해요? 회귀 테스트를 하지 않았어요.

페드로: 우리가 한 것은 단지 기능을 기존에 이미 테스트된 StandardBar에 위임한 것일 뿐이에요.

이브: 하지만 여전히 BarDatabase에서 쓰다 남은 재료를 제거하고 있어요.

페드로: 그것들은 분리되어 있어요.

이브: 오…​

페드로: 클로저는 다른 방식이 있나요?

이브: 글쎄요, 모르겠네요. 제가 아는 바로는 함수 합성을 사용한다는 거죠.

페드로: 설명해 보세요.

이브: IBar 구현은 함수의 집합이고, 다른 IBar는 또다른 함수의 집합이죠. 당신이 추가적으로 구현해야 할 모든 것은 함수 합성으로 다 취급되요. make-drink하고, 그 다음 bar에서 subtract-ingredients 하는 것과 같은 거죠.

페드로: 코드를 보여주시는 게 더 좋지 않을까요?

이브: 네, 하지만 뭐 별로 여기서 특별한 것은 없어요.

;; interface
(defprotocol IBar
  (make-drink [this drink]))

;; Bart's implementation
(deftype StandardBar []
  IBar
  (make-drink [this drink]
    (println "Making drink " drink)
    :ok))

;; our implementation
(deftype ProxiedBar [db ibar]
  IBar
  (make-drink [this drink]
    (make-drink ibar drink)
    (subtract-ingredients db drink)))

;; this how it was before
(make-drink (StandardBar.)
    {:name "Manhattan"
     :ingredients [["Bourbon" 75] ["Sweet Vermouth" 25] ["Angostura" 5]]})

;; this how it becomes now
(make-drink (ProxiedBar. {:db 1} (StandardBar.))
    {:name "Manhattan"
     :ingredients [["Bourbon" 75] ["Sweet Vermouth" 25] ["Angostura" 5]]})

이브: 함수의 집합을 단일 객체로 그룹화하기 위해 프로토콜과 타입을 이용하죠.

페드로: 클로저가 객체지향 능력 또한 갖춘 것처럼 보이네요.

이브: 맞아요, 더욱이 reify함수가 있는데, 이것을 이용하면 runtime에 프록시를 만들 수 있어요.

페드로: 런타임용 class같은 건가요?

이브: 그 비슷하죠.

(reify IBar
  (make-drink [this drink]
    ;; implementation goes here
  ))

페드로: 간편해 보이네요.

이브: 네, 하지만 전 여전히 이것이 데코레이터 패턴과 어떻게 다른지 이해가 안돼요.

페드로: 그것들은 완전히 다르죠.

이브: 데코레이터는 같은 인터페이스에 기능을 추가하는데, 프록시도 마찬가지죠.

페드로: 글쎄요, 하지만 프록시는…​

이브: 더욱이, 어댑터 또한 그리 다르지 않아요.

페드로: 어댑터는 다른 인터페이스를 이용하죠.

이브: 하지만 구현의 관점에서 보면 이런 패턴들은 모두 같아요. 즉, 어떤 것을 랩핑하고 랩퍼가 그것을 호출하죠. 그래서 "랩퍼"가 이런 패턴들에 더 맞는 이름일 수도 있어요.

에피소드 23: 브릿지(Bridge) 패턴

Hurece’s Sour Man의 채용 대행사 여직원들이 구인 요청에 맞는 후보들를 확인하고 있다. 문제는 직무는 고객들이 만드는데, 자격 요건은 채용 대행사에서 만든다는 것이다. 그들이 유연하게 협업할 수 있는 방법을 제공하라.

이브: 문제를 이해하지 못 하겠어요.

페드로: 저는 조금 경험이 있어요. 채용 대행사의 시스템이 아주 이상한데요, 자격 요건들을 인터페이스로 정의해요.

interface JobRequirement {
    boolean accept(Candidate c);
}

페드로: 모든 구체적인 자격 요건은 이 인터페이스를 구현한 것이에요.

class JavaRequirement implements JobRequirement {
    public boolean accept(Candidate c) {
        return c.hasSkill("Java");
    }
}

class Experience10YearsRequirement implements JobRequirement {
    public boolean accept(Candidate c) {
        return c.getExperience() >= 10;
    }
}

이브: 무슨 말인지 알겠어요.

페드로: 여기서 고려해야 할 점은, 이 자격 요건의 계층도가 채용 대행사에서 설계된다는 거죠.

이브: 그렇군요.

페드로: 그리고 Job 계층도가 있는데요, 또한 특정 직무(job)들은 이 Job의 서브 클래스들이죠.

이브: 왜 각 직무마다 클래스가 있어야 되죠? 그건 객체여야 해요.

페드로: 이 시스템은 객체보다는 클래스가 더 인기가 있을 때 설계되었죠. 그래서 여지껏 그대로에요.

이브: 클래스가 객체보다 인기가 있었다고요?!

페드로: 네, 잘 들어요. 자격 요건이 있는 직무는 완전히 분리된 계층도인데요, 그것은 고객에 의해 개발돼요. 그래서 이 두 계층도를 브릿지 패턴으로 분리해서, 각자가 독자적으로 유지되도록 해보죠.

abstract class Job {
    protected List<? extends JobRequirement> requirements;

    public Job(List<? extends JobRequirement> requirements) {
        this.requirements = requirements;
    }

    protected boolean accept(Candidate c) {
        for (JobRequirement j : requirements) {
            if (!j.accept(c)) {
                return false;
            }
        }
        return true;
    }
}

class CognitectClojureDeveloper extends Job {
    public CognitectClojureDeveloper() {
        super(Arrays.asList(
                  new ClojureJobRequirement(),
                  new Experience10YearsRequirement()
        ));
    }
}

이브: 그래서 브리지는 어디에 있죠?

페드로: JobRequirement, JavaRequirement, ExperienceRequirement이 한 계층도죠?

이브: 네.

페드로: Job, CongnitectClojureDeveloperJob, OracleJavaDeveloperJob이 또 다른 계층도죠.

이브: 오, 이제 알겠어요. JobJobRequirement의 링크가 브릿지네요.

페드로: 맞아요! 이것이 채용 대행사가 이 시스템을 이용해서 후보자를 찾는 방법이예요.

Candidate joshuaBloch = new Candidate();
(new CognitectClojureDeveloper()).accept(joshuaBloch);
(new OracleSeniorJavaDeveloper()).accept(joshuaBloch);

페드로: 여기에 포인트가 있어요. 고객은 Job 은 추상으로, JobRequirement는 구현으로 사용하죠. 고객들은 단지 직무(job)에 설명을 달아서 만들고, 채용 대행사는 그 설명을 특정 JobRequirement의 집합으로 바꾸죠.

이브: 알겠어요.

페드로: 제가 지금까지 이해한 바로는 클로저는 이 패턴을 defprotocoldefrecord로 흉내낼 수 있겠네요.

이브: 네, 하지만 이 문제 자체를 다시 보고 싶어요

페드로: 뭐가 잘못되었나요?

이브: 여기에는 일정한 절차가 있어요. 고객은 job 포지션을 만들고, 채용 대행사는 job 포지션을 자격 요건의 집합으로 바꾸고, 그들의 후보자 데이타베이스에서 맞는 사람들 고르는 스크립트를 실행합니다.

페드로: 맞아요.

이브: 그래서 이미 의존성이 있는 거죠. 왜냐하면 채용 대행사가 채용 직무 없이는 아무것도 할 수 없으니까요.

페드로: 글쎄요, 네. 하지만 채용 대행사는 자격 요건 체계를 만들 수 있어요. 채용 직무가 무엇인지 몰라도.

이브: 왜 그렇게 해야 하죠?

페드로: 나중에 이것은 Job 생성자에서 재사용할 수 있고, 그래서 채용 대행사는 같은 일을 두 번 하지 않아도 돼요.

이브: 오케이, 알겠어요, 하지만 이 문제는 인위적이에요. 기본적으로 우리가 필요한 것은 추상과 구체 사이의 협업 방법입니다.

페드로: 아마도요. 하지만 브릿지 패턴으로 이 특수한 문제를 클로저로 어떻게 푸는지 알고 싶어요.

이브: 쉽죠. adhoc 계층도를 사용합시다.

페드로: 추상용 계층도인가요?

이브: 네, job 계층도는 추상이죠, 그리고 사람들은 계층도를 보강할 필요가 있어요.

;; abstraction

(derive ::clojure-job ::job)
(derive ::java-job ::job)
(derive ::senior-clojure-job ::clojure-job)
(derive ::senior-java-job    ::java-job)

이브: 채용 대행사는 개발자들과 같이 마찬가지로, 이 추상의 구현을 제공합니다.

;; implementation

(defmulti accept :job)

(defmethod accept :java [candidate]
  (and (some #{:java} (:skills candidate))
       (> (:experience candidate) 1)))

이브: 나중에, 새 채용 직무가 만들어졌는데 자격 요건은 개발되어 있지 않고, 그러한 job 타입을 위한 accept 메소드 구현도 없으면, adhoc 계층도의 기존 accept들이 사용돼요.

페드로: 흠?

이브: 누군가 새로운 ::senior-java를 :java의 일의 자식으로 만들었다고 합시다.

페드로: 오, 그리고 채용 대행사는 분기값이 ::senior-java인 accept 메소드 구현을 제공하지 않으면, 분기값이 ::java인 메소드가 호출되는 건가요?

이브: 당신은 빠르게 배우는 군요.

페드로: 하지만 이것이 정말 브릿지 패턴인가요?

이브: 여기에는 브릿지가 없어요, 하지만 추상과 구체가 독립적으로 유지될 수 있습니다.

끝.

정리

패턴을 이해한다는 것은 상당히 혼동스런 작업인데, 흔히 UML 다이어그램이나 이상한 이름들을 사용해서 객체지향 방식으로 제시되거나, 특정 언어에 국한한 문제를 풀기 위한다거나 하기 때문이다. 그래서 여기에 작은 치트 쉬트를 공안했는데, 유삿성을 통해 패턴을 이해하는데 도움이 될 것이다.

  • 커맨드 패턴 - 함수

  • 전략 패턴 - 함수를 인수로 받는 함수.

  • 상태 패턴 - 상태에 의존하는 전략 패턴.

  • 방문자 패턴 - 다중 디스패치.

  • 템플릿 메소드 패턴 - 기본 값을 포함한 전략 패턴.

  • 이터레이터 패턴 - 시퀀스

  • 메멘토 패턴 - 저장과 복구

  • 관찰자 패턴 - 다른 함수뒤에 호출되는 함수.

  • 인터프리터 패턴 - 트리를 처리하는 함수들.

  • 플라이웨이트 패턴 - 캐쉬.

  • 빌더 패턴 - 선택 인수.

  • 책임 연쇄 패턴 - 함수 합성.

  • 합성 패턴 - 트리

  • 팩토리 메소드 패턴 - 객체 생성 전략.

  • 추상 팩토리 패턴 - 관련 객체 생성 전략.

  • 어댑터 패턴 - 랩퍼, 같은 기능들, 다양한 타입.

  • 데코레이터 패턴 - 랩퍼, 같은 타입, 새로운 기능.

  • 프록시 패턴 - 랩퍼, 함수 합성.

  • 브릿지 패턴 - 추상과 구체의 분리.

등장 인물

아주 오래전 우주 저 멀리 먼 곳에…

상상력의 부족으로 인해 모든 캐릭터들과 이름들은 단지 다음과 같이 회문(anagram)으로 만들었다.

  • Pedro Veel - Developer

  • Eve Dopler - Developer

  • Serpent Hill & R.E.E. - Enterprise Hell

  • Sven Tori - Investor

  • Karmen Git - Marketing

  • Natanius S. Selbys - Business Analyst

  • Mech Dominore Fight Saga - Heroes of Might and Magic

  • Kent Podiololis - I don’t like loops

  • Chad Bogue - Douchebag

  • Dex Ringeus - UX Designer

  • Veerco Wierde - Code Review

  • Dartee Hebl - Heartbleed

  • Bertie Prayc - Cyber Pirate

  • Cristopher, Matton & Pharts - Important Charts & Reports

  • Tuck Brass - Starbucks

  • Eugenio Reinn Jr. - Junior Engineer

  • Feverro O’Neal - Forever Alone

  • A Profit NY - Profanity

  • Bella Hock - Black Hole

  • Sir Dry Bang - Angry Birds

  • Saimank Gerr - Risk Manager

  • Deam Evil - Medieval

  • Podrea Vesper - Eavesdropper

  • Deren Bart - Bartender

  • Hurece’s Sour Man - Human Resources

P.S. 나는 이 기사의 작성을 2년 훨씬 전부터 시작했다. 시간을 흘러, 모든 것이 변해 자바 8이 나왔다.

mishadoff 18 December 2015


1. Double-checked locking: Clever, but broken
2. 지금은 고인이 된 미국 스탠드업 코미디의 대부. 그의 유머 소재는 주로 미국 현대사회의 어두운 이면에 집중되어 있으며, 가끔 영어 표현의 부조리함도 주 소재로 삼곤 했다. 미정부의 공중파에서의 부적절한 내용에 대한 규제를 용인한 미법원의 판결을 주제로 한 “7가지 비속어들"(Seven Dirty Words)이라는 공연을 하고 공연 직후 경찰에 체포 되었다. 이 사건으로 그는 더 유명해졌다.