Chain of Responsibility는 핸들러를 가진 객체가 요청을 받으면 자신이 처리할 요청인지 확인 후, 맞다면 처리하고 아니라면 다음 객체에게 전달한다. 전달받은 객체도 동일한 핸들러 인터페이스를 갖고 있으며, 전달받은 요청을 확인 후 마찬가지로 처리한다. 이렇게 핸들러들이 체인처럼 엮여서 마치 필터를 거치듯 필요한 객체까지 전달되어 처리되는 방식이 Chain of Responsibility Pattern이다.
이 패턴을 사용하는 곳은 아주 흔하게 보이는데, 그 중 하나가 예외처리에 사용되는try-catch 문이다. Exception이 발생하면 try-catch에서 잡아내거나, 잡아내지 못하면 콜스택 위로 던져준다. 이 과정은 exception이 잡힐 때까지 체인처럼 동작한다.

또 다른 예는 윈도우 시스템의 이벤트 핸들링이다. 버튼이나 레이블 위젯에 마우스 클릭 이벤트가 발생하면, 버튼은 해당 객체에서 핸들러가 수행되겠지만 핸들러가 없는경우 부모 윈도우로 이벤트를 넘겨주게된다.

예에서 보듯이 스택형태의 레이어들로 이루어진 시스템에서 매우 흔한 일이다. 예전 안드로이드 개발할 때 블루투스의 키 이벤트인 플레이, 일시정지, 정지, 다음곡등을 오디오 스택에서 이런식으로 처리했던 기억이 있다. 오디오 처리하는 부분이 스택처럼 되어 있고 최근 실행된 어플리케이션이 최상단에 존재해서 이벤트를 받아 처리한다. 만약, 사용하지 않는 이벤트가 들어온다면 스택을 따라 아래로 이벤트를 내려주게된다.
구현예로는 안드로이드에서 쓰는 것과 유사한 로그 시스템을 만들어보자. 안드로이드에선 Verbose(v), Debug(d), Info(i), Warn(w), Error(e) 다섯가지 레벨을 사용한다. 순서대로 뒤의 단계들을 다 포함한다. 예를들어 Debug(d)는 디버깅용 메세지로, v level을 제외하고 i, w, e에 해당하는 메세지를 모두 출력한다. 우선, 추상 클래스를 만들어보자.
public enum LOGLEVEL{E("[ERROR]"), D("[DEBUG]"), V("[VERBOSE]");
protected final String type;
LOGLEVEL(String s) {
this.type = s;
}
}
public abstract class MyLogger {
protected MyLogger nextLogger = null;
protected LOGLEVEL level = null;
private static LOGLEVEL outputLevel = LOGLEVEL.V;
public abstract void handleMessage(LOGLEVEL level, String msg);
public void setNextLogger(MyLogger logger){
nextLogger = logger;
}
public void sendToNext(LOGLEVEL level, String msg){
if(this.level.ordinal() < outputLevel.ordinal() && nextLogger != null){
nextLogger.handleMessage(level, msg);
}
}
public static void setLevel(LOGLEVEL level){
outputLevel = level;
}
public static LOGLEVEL getLevel(){
return outputLevel;
}
}
먼저 로그레벨에 대한 enum 클래스를 만들었다. 예제이므로 3단계만 만들기로 한다. 요청을 처리하는 handleMessage() 는 서브클래스들에서 구현될 메쏘드이고, 마치 링크드 리스트처럼 체인으로 연결하기위해 nextLogger를 사용했다. sendToNext()는 공통으로 사용가능해서 여기서 구현했다. 상속을 받아 각 로거들을 만들어보자.
public class VerboseLogger extends MyLogger {
VerboseLogger(){
level = LOGLEVEL.V;
}
@Override
public void handleMessage(LOGLEVEL level, String msg) {
if(this.level == level){
System.out.println("VerboseLogger: " + level.type + msg);
}
sendToNext(level, msg);
}
}
public class DebugLogger extends MyLogger {
DebugLogger(){
level = LOGLEVEL.D;
}
@Override
public void handleMessage(LOGLEVEL level, String msg) {
if(this.level == level){
System.out.println("DebugLogger: " + level.type + msg);
}
sendToNext(level, msg);
}
}
public class ErrorLogger extends MyLogger {
ErrorLogger(){
level = LOGLEVEL.E;
}
@Override
public void handleMessage(LOGLEVEL level, String msg) {
if(this.level == level){
System.out.println("ErrorLogger: " + level.type + msg);
}
sendToNext(level, msg);
}
각 구현 클래스에서 출력포맷을 마음대로 만질 수 있고, 여기서는 간단하게 어느 클래스에서 출력하는지만 표시해줬다. sendToNext()는 추상 클래스 MyLogger에서 구현했는데, output level에 따라 리퀘스트 메세지를 전달할지 결정하도록 추가해봤다.
클라이언트 코드는 다음과 같이 만들었다.
public class MyLog {
private static MyLogger vLogger = new VerboseLogger();
private static MyLogger dLogger = new DebugLogger();
private static MyLogger eLogger = new ErrorLogger();
static {
eLogger.setNextLogger(dLogger);
dLogger.setNextLogger(vLogger);
}
public static void log(LOGLEVEL level, String msg){
eLogger.handleMessage(level, msg);
}
public static void setLevel(LOGLEVEL level){
MyLogger.setLevel(level);
}
public static LOGLEVEL getLevel(){
return MyLogger.getLevel();
}
}
각각의 concrete class instances를 생성해주고 체인처럼 연결해줬다. eLogger에만 전달하면 주어진 레벨을 차례대로 확인하면서 알아서 출력할 것이다. 뽀너스로 setLevel(), getLevel()을 구현했는데, output 레벨을 설정하는 메쏘드이다. 설정된 output level을 넘어가면 메세지 체인을 멈추도록 MyLogger.sendToNext()에 구현되어 있다.
테스트 실행코드는 다음과 같다.
public class Main {
public static void main(String[] args){
MyLog.log(LOGLEVEL.D, "It's debug message");
MyLog.log(LOGLEVEL.V, "It's verbose message");
MyLog.log(LOGLEVEL.E, "It's error message");
System.out.println("--- Set output level DEBUG ---");
MyLog.setLevel(LOGLEVEL.D);
MyLog.log(LOGLEVEL.D, "It's debug message");
MyLog.log(LOGLEVEL.V, "It's verbose message");
MyLog.log(LOGLEVEL.E, "It's error message");
}
}
DebugLogger: [DEBUG]It's debug message
VerboseLogger: [VERBOSE]It's verbose message
ErrorLogger: [ERROR]It's error message
--- Set output level DEBUG ---
DebugLogger: [DEBUG]It's debug message
ErrorLogger: [ERROR]It's error message
구현하면서 조금 애매한 부분은 LOGLEVEL enum 클래스였다. 외부에서도 사용할 수 있게 개별 클래스로 만들긴 했는데, 그냥 추상 클래스에 상수로 정의해도 될거같다.
또 한가지는 클라이언트인 MyLog 클래스에서 패턴의 핵심인 체인을 만들어주고 있다. 트리형태로 구성되는 윈도우 시스템의 경우, 생성시 체인이 되는 parent를 지정해준다. 여기서도 생성자에서 처리해주는게 더 나았을수도 있겠단 생각이 들었다.
마지막으로 Chain of Responsibility UML 다이어그램 표현은 다음과 같다.
