행동 관련 : 책임 연쇄 패턴

    들어가기 전

    이 글은 백기선님의 GOF 강의를 복습하며 작성한 글입니다. 

     


    책임 연쇄 패턴 

    • GOF : 요청을 보내는 쪽(Sender)와 처리하는 쪽(Receiver)를 분리하는 패턴임.
      • Sender는 Receiver의 구체적인 타입을 모른채로 요청을 보냄 → Sender / Receiver가 Decoupling 됨.
      • 클라이언트(Sender)는 추상화된 Handler에만 의존하고, Handler 내부에서 ConcreteHandler끼리 체이닝으로 처리됨. 
      • 이를 통해 요청을 처리할 수 있는 여러 객체 중 하나가 요청을 처리하도록 한다.
    • Concrete Handler를 분리하면, Handler를 책임 단위로 분리할 수 있음. 
      • 단일 책임 원칙 (SRP)를 지키기 편리함.
      • 새로운 Handler가 추가되어도 기존 코드에 변경점이 적음. (OCP)
    • 책임을 잘게 나누고 Handler 내에서 체이닝 형태로 처리되기 때문에  '책임 연쇄 패턴'이라고 이해할 수 있다. 

    요청 + 응답 처리할 때 굉장히 많이 사용되는 패턴이다. Request / Handler가 디커플링된 채로 Request를 처리할 수 있다는 점이 좋다. 

     


    디자인 패턴이 필요한 코드 

    // Receiver
    public class RequestHandler {
    
        public void handler(Request request) {
            System.out.println(request.getBody());
        }
    }
    
    // 클라이언트 코드 (Sender)
    public class Client {
    
        public static void main(String[] args) {
            Request request = new Request("bloom moo-goon-wha.");
            RequestHandler requestHandler = new RequestHandler();
            requestHandler.handler(request);
        }
    }

    다음 코드를 가정해보자.

    • Client는 Request 인스턴스를 만든 후, 요청을 RequestHandler에게 전달함.
    • RequestHandler는 요청을 처리함. 

    현재 RequestHandler는 Request Body에 있는 값을 꺼내서 출력해주고 있다. 그런데 만약 이런 기능이 추가된다고 하면 어떻게 될까? 

    1. 출력 전에 로그 찍기
    2. 출력 전에 접근 가능한 녀석인지 확인하는 인증 / 인가를 추가

    현재의 RequestHandler에서는 위 기능을 추가할 수 있는 방법은 무엇이 있을까?

    • 기존 코드를 수정하기
    • RequestHandler 상속받은 후, 기능을 추가하고 super().handle()로 처리하기

    첫번째 방법은 확장성 관점에서 좋지 않으며, 두번째 코드 역시 마찬가지다. 만약 어떤 때는 로그만 찍고, 어떤 때는 인증 / 인가 기능만 사용한다고 하면 조합의 갯수만큼 RequestHandler 클래스를 만들어야 할 것이다.

    이런 형태의 문제점은 Sender와 Receiver가 결합되어 있기 때문이다. 예를 들어 인증 기능이 필요한 경우 Sender는 AuthRequestHandler를 만들어서 요청을 보내야한다. 로깅이 필요한 경우 LoggingRequestHandler를 사용해야한다. 클라이언트가 자신의 의도에 따라 어떤 RequestHandler를 사용할지 결정한다는 것은 Sender와 Receiver가 결합되었다는 것을 의미한다. 

    이 부분은 책임 연쇄 패턴으로 Sender / Receiver를 디커플링해서 개선 해볼 수 있다. 


    디자인 패턴 적용하기

    Sender가 어떤 Receiver를 사용해야하는지 모르게 해야한다. 따라서 Receiver를 인터페이스로 추상화하고, Concrete Receiver를 여러 개 만들어서 책임을 분리하고, Sender는 추상화된 Receiver 인터페이스로만 요청을 보내도록 해야한다.  이 때 책임을 연쇄적으로 처리하기 위해서는 LinkedList 형태로 만들어도 괜찮고, 혹은 배열에 필요한 Handler를 넣고 Iteration을 돌려도 된다. 

    여기서는 LinkedList로 처리하려고 한다. 

    public abstract class RequestHandler {
    
        private final RequestHandler next;
    
        public RequestHandler(RequestHandler next) {
            this.next = next;
        }
    
        public void handle(Request request) {
            if (this.next != null) {
                this.next.handle(request);
            }
        }
    
    }

    LinkedList 형태로 사용하려고 하기 때문에 RequestHandler를 추상 클래스로 생성한다. 그리고 Next Node가 있는 경우에는 Next를 계속 호출하는 형태로 처리한다. 여기서 RequestHandler 추상 클래스에 있는 handle() 메서드는 일종의 Template 메서드로 제공되는 것이다.

    public class AuthRequestHandler extends RequestHandler {
    
        public AuthRequestHandler(RequestHandler next) { super(next); }
    
        @Override
        public void handle(Request request) {
            System.out.println("can auth?");
            System.out.println("can use this handler?");
            super.handle(request);
        }
    }
    
    public class LoggingRequestHandler extends RequestHandler {
        public LoggingRequestHandler(RequestHandler next) { super(next); }
    
        @Override
        public void handle(Request request) {
            System.out.println("logging");
            super.handle(request);
        }
    }

    다음과 같이 여러 Concrete Handler를 생성한 후, 생성자를 통해 다음 Node를 주입받고 호출하는 형태로 구현한다.

    public class Client {
    
        public void doSomething(RequestHandler requestHandler){
            Request request = new Request("bloom moo-goon-wha.");
            requestHandler.handle(request);
    
        }
    
        public static void main(String[] args) {
            RequestHandler handler = new AuthRequestHandler(new LoggingRequestHandler(new PrintRequestHandler(null)));
            Client client = new Client();
            client.doSomething(handler);
        }
    }

    이 Handler를 사용하는 클라이언트 코드다. 이 코드에서 주의해서 볼 부분은 다음과 같다.

    • Client는 어떤 Handler를 사용해야 할지 결정하지 않음. (외부에서 주입해줌) 
    • Client는 추상화 된 Handler의 인터페이스만 사용함.

    다시 한번 정리하지만 Client는 자세한 Handler의 내용을 모르고, 추상화 된 부분에만 집중한다. 이를 통해 Sender / Receiver는 분리된다. 

     


    책임 연쇄 패턴 장/단점

    •  장점
      • 클라이언트 코드를 변경하지 않고 새로운 Handler를 핸들러 체인에 추가하거나 순서를 바꿀 수 있음. (OCP)
      • 체인을 다양한 형태로 구현할 수 있음.
        • 순서가 중요한 체이닝
        • LinkedList 형태 / 배열 형태
    • 단점
      • 체인 때문에 코드의 흐름이 많아짐. 따라서 디버깅이 번거로워짐. 

    책임 연쇄 패턴은 Sender로부터 Receiver과 추상화되어 완전히 캡슐화 되었다. 따라서 새로운 Receiver가 추가되더라도 Sender에는 전혀 영향을 미치지 않는다. Receiver쪽 코드 확장이 편리하게 된다는 것을 의미한다.

    단점으로는 Receiver가 추상회 되었기 때문에 추상화에 대한 비용이 발생한다. 가장 큰 비용으로는 디버깅 비용의 증가로 볼 수 있다.

     

    댓글

    Designed by JB FACTORY