정적으로 어떤 기능에 대한 처리의 연결이 하드코딩 되어 있을 때 기능 처리의 연결 변경이 불가능한데, 이를 동적으로 연결되어 있는 경우에 따라 다르게 처리될 수 있도록 연결한 패턴이다.
즉, 클라이어트의 요청에 대한 세세한 처리를 하나의 객체가 몽땅 하는 것이 아닌, 여러개의 처리 객체들로 나누고, 이들을 사슬(chain) 처럼 연결해 집합 안에서 연쇄적으로 처리하도록 한다.
이러한 처리 객체들을 핸들러(handler)라고 부르는데, 요청을 받으면 각 핸들러는 요청을 처리하거나, 처리할 수 없을 경우 체인의 다음 핸들러로 처리에 대한 책임을 전가한다. 한마디로 책임 연쇄라는 말은 요청에 대한 책임을 다른 객체에 떠넘긴다는 소리이다. 떠넘긴다고 하니까 부정적인 의미로 들릴수도 있겠지만, 이러한 체인 구성은 하나의 객체에 처리에 대한 책임을 요청을 보내는 쪽(sender)과 요청을 처리하는(receiver) 쪽을 분리하여 각 객체를 부품으로 독립시키고 결합도를 느슨하게 만들며, 상황에 따라서 요청을 처리할 객체가 변하는 프로그램에도 유연하게 대응할 수 있다. 특히나 중첩 if-else문들을 최적화하는데 있어 실무에서도 많이 애용되는 패턴중 하나이기도 하다.
- Handler : 요청을 수신하고 처리 객체들의 집합을 정의하는 인터페이스
- ConcreteHandler : 요청을 처리하는 실제 처리 객체
핸들러에 대한 필드를 내부에 가지고 있으며 메서드를 통해 다음 핸들러를 체인시키고 다음 체인을 바라본다.
자신이 처리할 수 없는 요구가 나오면 바라보고 있는 다음 체인의 핸들러에게 요청을 떠넘긴다.
ConcreteHandler1 - ConcreteHandler2 - ConcreteHandler3 - ... 이런식으로 체인 형식이 구성되게 된다.- Client : 요청을 Handler 전달한다
여기서 핸들러끼리 체이닝 되는 구조는 어떤 형태이든 상관이 없다. 리스트형 일수도 있고 선형 일 수도 있고 트리 형태일 수도 있다.
// UrlParser.java
public class UrlParser {
public static void run(String url) {
// protocla 파싱
int index = url.indexOf("://");
if (index != -1) {
System.out.println("PROTOCOL : " + url.substring(0, index));
} else {
System.out.println("PROTOCOL : NOT FOUND");
}
// domain 파싱
int startIndex = url.indexOf("://");
int lastIndex = url.lastIndexOf(":");
System.out.print("DOMAIN : ");
if (startIndex == -1) {
if (lastIndex == -1) {
System.out.println(url);
} else {
System.out.println(url.substring(0, lastIndex));
}
} else if (startIndex != lastIndex) {
System.out.println(url.substring(startIndex + 3, lastIndex));
} else {
System.out.println(url.substring(startIndex + 3));
}
// port 파싱
int portIndex = url.lastIndexOf(":");
if (portIndex != -1) {
String portString = url.substring(portIndex + 1);
try {
int port = Integer.parseInt(portString);
System.out.println("PORT : " + port);
} catch (NumberFormatException e) {
System.out.println("PORT : NOT FOUND");
}
}
}
}
// Client.java
public class Client {
public static void main(String[] args) {
String url1 = "www.youtube.com:80";
System.out.println("INPUT : " + url1);
UrlParser.run(url1);
String url2 = "https://velog.io/@dnjsdn96";
System.out.println("INPUT : " + url2);
UrlParser.run(url2);
String url3 = "http://localhost:8080";
System.out.println("INPUT : " + url3);
UrlParser.run(url3);
}
}
// 실행 결과
INPUT : www.youtube.com:80
PROTOCOL : NOT FOUND
DOMAIN : www.youtube.com
PORT : 80
INPUT : https://velog.io/@dnjsdn96
PROTOCOL : https
DOMAIN : velog.io/@dnjsdn96
PORT : NOT FOUND
INPUT : http://localhost:8080
PROTOCOL : http
DOMAIN : localhost
PORT : 8080
전체적으로 동작은 정상적으로 하지만 이 코드의 문제는 만약 path정보를 별도로 분리해서 출력해야 한다거나 추가적인 작업이 필요할 경우 코드를 전체적으로 수정해야 한다는 점이다.
또한 만약 포트정보는 출력되지 않기를 바라는 url이 있을 경우 포트번호를 출력하지 않는 메소드를 별도로 만들어야 한다.
// UrlHandler.java (Handler) => 추상 클래스로 작성
abstract class UrlHandler {
// 다음 실행될 핸들러
protected UrlHandler nextHandler = null;
// 생성자를 통해 연결시킬 핸들러를 등록
public UrlHandler setNextHandler(UrlHandler handler) {
this.nextHandler = handler;
return handler; // 메서드 체이닝 구성을 위해 인자를 그대로 반환
}
// 자식 핸들러에서 구체화 하는 추상 메서드
protected abstract void process(String url);
// 핸들러가 요청에 대해 처리하는 메서드
public void run(String url) {
process(url);
if (nextHandler != null) nextHandler.run(url);
}
}
// ProtocolHandler.java (ConcreteHandler)
public class ProtocolHandler extends UrlHandler {
@Override
public void process(String url) {
int index = url.indexOf("://");
if (index != -1) {
System.out.println("PROTOCOL : " + url.substring(0, index));
} else {
System.out.println("PROTOCOL : NOT FOUND");
}
}
}
// DomainHandler.java (ConcreteHandler)
public class DomainHandler extends UrlHandler {
@Override
public void process(String url) {
int startIndex = url.indexOf("://");
int lastIndex = url.lastIndexOf(":");
System.out.print("DOMAIN : ");
if (startIndex == -1) {
if (lastIndex == -1) {
System.out.println(url);
} else {
System.out.println(url.substring(0, lastIndex));
}
} else if (startIndex != lastIndex) {
System.out.println(url.substring(startIndex + 3, lastIndex));
} else {
System.out.println(url.substring(startIndex + 3));
}
}
}
// PortHandler.java (ConcreteHandler)
public class PortHandler extends UrlHandler {
@Override
public void process(String url) {
int portIndex = url.lastIndexOf(":");
if (portIndex != -1) {
String portString = url.substring(portIndex + 1);
try {
int port = Integer.parseInt(portString);
System.out.println("PORT : " + port);
} catch (NumberFormatException e) {
System.out.println("PORT : NOT FOUND");
}
}
}
}
// Client.java (Client)
public class Client {
public static void main(String[] args) {
UrlHandler protocolHandler = new ProtocolHandler();
UrlHandler domainHandler = new DomainHandler();
UrlHandler portHandler = new PortHandler();
protocolHandler.setNextHandler(domainHandler).setNextHandler(portHandler);
String url1 = "www.youtube.com:80";
System.out.println("INPUT : " + url1);
protocolHandler.run(url1);
String url2 = "https://velog.io/@dnjsdn96";
System.out.println("INPUT : " + url2);
protocolHandler.run(url2);
String url3 = "http://localhost:8080";
System.out.println("INPUT : " + url3);
protocolHandler.run(url3);
}
}
// 실행 결과
INPUT : www.youtube.com:80
PROTOCOL : NOT FOUND
DOMAIN : www.youtube.com
PORT : 80
INPUT : https://velog.io/@dnjsdn96
PROTOCOL : https
DOMAIN : velog.io/@dnjsdn96
PORT : NOT FOUND
INPUT : http://localhost:8080
PROTOCOL : http
DOMAIN : localhost
PORT : 8080
전략 패턴이 전략 알고리즘 코드를 객체화 한 것이고, 상태 패턴은 객체의 상태를, 명령 패턴이 커맨드를 객체화 한 것과 같이, 책임 연쇄 패턴은 조건문의 요청 처리 로직 자체를 객체화 한 것으로 보면 된다. 그리고 일반적인 조건 분기문 같은 경우 다중으로 구성될 수 있으니 이를 체인으로 객체끼리 연결함으로써, 각 if문 로직을 클래스로 표현하였다고 보면 된다.