[디자인패턴] 책임 연쇄 패턴 (Chain of Responsibility Pattern)

koline·2023년 9월 6일
0

디자인패턴

목록 보기
14/24

책임 연쇄 패턴


정적으로 어떤 기능에 대한 처리의 연결이 하드코딩 되어 있을 때 기능 처리의 연결 변경이 불가능한데, 이를 동적으로 연결되어 있는 경우에 따라 다르게 처리될 수 있도록 연결한 패턴이다.

즉, 클라이어트의 요청에 대한 세세한 처리를 하나의 객체가 몽땅 하는 것이 아닌, 여러개의 처리 객체들로 나누고, 이들을 사슬(chain) 처럼 연결해 집합 안에서 연쇄적으로 처리하도록 한다.

이러한 처리 객체들을 핸들러(handler)라고 부르는데, 요청을 받으면 각 핸들러는 요청을 처리하거나, 처리할 수 없을 경우 체인의 다음 핸들러로 처리에 대한 책임을 전가한다. 한마디로 책임 연쇄라는 말은 요청에 대한 책임을 다른 객체에 떠넘긴다는 소리이다. 떠넘긴다고 하니까 부정적인 의미로 들릴수도 있겠지만, 이러한 체인 구성은 하나의 객체에 처리에 대한 책임을 요청을 보내는 쪽(sender)과 요청을 처리하는(receiver) 쪽을 분리하여 각 객체를 부품으로 독립시키고 결합도를 느슨하게 만들며, 상황에 따라서 요청을 처리할 객체가 변하는 프로그램에도 유연하게 대응할 수 있다. 특히나 중첩 if-else문들을 최적화하는데 있어 실무에서도 많이 애용되는 패턴중 하나이기도 하다.



구조


  1. Handler : 요청을 수신하고 처리 객체들의 집합을 정의하는 인터페이스
  2. ConcreteHandler : 요청을 처리하는 실제 처리 객체
    핸들러에 대한 필드를 내부에 가지고 있으며 메서드를 통해 다음 핸들러를 체인시키고 다음 체인을 바라본다.
    자신이 처리할 수 없는 요구가 나오면 바라보고 있는 다음 체인의 핸들러에게 요청을 떠넘긴다.
    ConcreteHandler1 - ConcreteHandler2 - ConcreteHandler3 - ... 이런식으로 체인 형식이 구성되게 된다.
  3. 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문 로직을 클래스로 표현하였다고 보면 된다.



목적

  1. 특정 요청을 2개 이상의 여러 객체에서 판별하고 처리해야 할때
  2. 특정 순서로 여러 핸들러를 실행해야 하는 경우
  3. 프로그램이 다양한 방식과 종류의 요청을 처리할 것으로 예상되지만 정확한 요청 유형과 순서를 미리 알 수 없는 경우
  4. 요청을 처리할 수 있는 객체 집합이 동적으로 정의되어야 할 때 (체인 연결을 런타임에서 동적으로 설정)

장점

  1. 클라이언트는 처리 객체의 체인 집합 내부의 구조를 알 필요가 없다.
  2. 각각의 체인은 자신이 해야하는 일만 하기 때문에 새로운 요청에 대한 처리객체 생성이 편리해진다.
  3. 클라이언트 코드를 변경하지 않고 핸들러를 체인에 동적으로 추가하거나 처리 순서를 변경하거나 삭제할 수 있어 유연해진다
  4. 요청의 호출자(invoker)와 수신자(receiver) 분리시킬 수 있다.
    4-1. 요청을 하는 쪽과 요청을 처리하는 쪽을 디커플링 시켜 결합도를 낮춘다
    4-2. 요청을 처리하는 방법이 바뀌더라도 호출자 코드는 변경되지 않는다.

단점

  1. 실행 시에 코드의 흐름이 많아져서 과정을 살펴보거나 디버깅 및 테스트가 쉽지 않다.
  2. 충분한 디버깅을 거치지 않았을 경우 집합 내부에서 무한 사이클이 발생할 수 있다.
  3. 요청이 반드시 수행된다는 보장이 없다. (체인 끝까지 갔는데도 처리되지 않을 수 있다)
  4. 책임 연쇄로 인한 처리 지연 문제가 발생할 수 있다.
    4-1. 다만 이는 트레이드 오프로서 요청과 처리에 대한 관계가 고정적이고 속도가 중요하면 책임 연쇄 패턴 사용을 유의하여야 한다.


참고


[디자인패턴] 디자인패턴이란? - 생성패턴, 구조패턴, 행위패턴

Chain Of Responsibility 패턴 - 완벽 마스터하기

profile
개발공부를해보자

0개의 댓글