open:chain-of-responsibility-pattern

Chain Of Responsibility Pattern

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

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

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

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

이브: 조지 칼린이요.

리스트를 보다가 웃으며

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

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

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

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"

페드로: 이해됐어요.

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


  • open/chain-of-responsibility-pattern.txt
  • 마지막으로 수정됨: 2021/11/22 11:11
  • 저자 127.0.0.1