# 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"
페드로: 이해됐어요.
이브: 책임 연쇄 패턴은 단지 함수 합성일 뿐인 거죠.