책임 연쇄 패턴 (Chain-of-Responsibility) 패턴

weekbelt·2022년 12월 15일
0

1. 패턴 소개

책임 연쇄 패턴은 영어로 Chain-of-Responsibility라고하는데 각각의 인스턴스의 책임들이 체인처럼 연쇄되어 있다는 뜻입니다. 요청을 보내는쪽과 요청을 처리하는 쪽을 분리시키는 패턴인데 요청을 보내는 쪽에서 그 요청을 처리하는 핸들러가 어떤 구체적인 타입인지는 상관없이 디커플링된 상태에서 요청을 처리할 수 있게끔 해주는 패턴입니다.

위 그림의 구조를 살펴보면 Handler라는 공통의 인터페이스 또는 추상클래스를 선언하고 그 클래스들을 구현한 ConcreteHandler를 연결되도록 하고 Client 그 체인이 어떤핸들러가 있는지 전혀 상관하지 않고 handlerRequest를 호출해 요청을 처리할 수 있습니다.

패턴 적용전 예시코드를 살펴보겠습니다. 아래 코드를 살펴보면 Request인스턴스를 생성해서 RequestHandler인스턴스의 handler메서드의 인자값으로 Request를 보내고 있습니다.

public class Client {

    public static void main(String[] args) {
        Request request = new Request("무궁화 꽃이 피었습니다.");
        RequestHandler requestHandler = new LoggingRequestHandler();
        requestHandler.handler(request);
    }
}

RequestHandler는 Request를 받아서 body를 출력하도록 하는 기능을 하고 있습니다.

public class RequestHandler {

    public void handler(Request request) {
        System.out.println(request.getBody());
    }
}

위와 같은 코드가 있다고 하고 Request요청을 처리하기전에 인증 또는 인가처리를 통해 인증된 사용자가 요청한 Request인지를 처리하는 부분이 필요하다고 가정합시다. 보통 쉽게 떠올릴수 있는 방법이 2가지가 있는데 첫번째는 RequestHandler의 handler메서드에 인증처리를 하는 로직을 추가하는 방법입니다.

public class RequestHandler {

    public void handler(Request request) {
    	// request를 출력하기 전에 인증 처리 로직을 추가
        quthentication(request);
        System.out.println(request.getBody());
    }
}

handler메서드에 인증처리하는 기능을 추가하면 단일책임원칙을 위배하게 됩니다. 그 이유는 기존의 RequestHandler는 Request의 body를 출력하는 책임만 있었는데 이제 인증처리하는 책임까지 2개의 책임이 생겼기 때문입니다. 그렇다면 다른 방법은 인증을 처리하는 책임을 가진 AuthRequestHandler를 정의해서 RequestHandler를 상속받고 인증처리를 한 후에 super클래스의 handler메서드를 호출해서 Request의 Body를 출력하도록 합니다.

public class AuthRequestHandler extends RequestHandler {

    public void handler(Request request) {
        System.out.println("인증이 되었나?");
        System.out.println("이 핸들러를 사용할 수 있는 유저인가?");
        super.handler(request);
    }
}

그렇다면 Client는 AuthRequestHandler인스턴스를 생성해서 인증 후 Request의 본문을 출력하도록 할 수 있습니다.

public class Client {

    public static void main(String[] args) {
        Request request = new Request("무궁화 꽃이 피었습니다.");
        RequestHandler requestHandler = new AuthRequestHandler();
        requestHandler.handler(request);
    }
}

위와 같은 방법은 단일 책임원칙을 지키게 됩니다. 기존의 RequestHandler는 Request의 본문을 출력하는 하나의 책임을 가지고 있고 AuthReqruestHandler또한 인증을 처리하는 하나의 책임을 가지고 있습니다. 하지만 Client가 직접 해당 인스턴스를 선택해야 합니다. 인증처리를 하고 싶으면 직접 AuthRequestHandler인스턴스를 사용해야합니다. 또 로깅을 해야하는 책임이 추가된다면 RequestHandler를 상속 받는 LoggingRequestHandler를 정의해서 Client는 LoggingRequestHandler를 사용해야 합니다.

public class LoggingRequestHandler extends RequestHandler {

    @Override
    public void handler(Request request) {
        System.out.println("로깅");
        super.handler(request);
    }
}

그렇다면 Logging도 하고 Authentication도 하고 싶다면 어떻게 써야 할까요? 이 문제부터 복잡해지기 시작합니다. 이렇게 복잡해지는 이유 중의 하나는 클라이언트가 사용해야하는 핸들러를 직접 알아야한다는 것입니다. 그래서 이 문제를 해결하기위해 책임을 연쇄적으로 적용을 할 것입니다.

2. 패턴 적용하기

책임연쇄패턴을 적용해서 기존 코드의 문제점들을 해결해 보겠습니다. 먼저 Handler역할의 추상클래스인 RequestHandler를 생성합니다. ConcreteHandler들이 연쇄적으로 이어질 수 있게끔 RequestHandler타입의 필드인 nextHandler를 선언하고 그 핸들러에게 Request처리를 위임하도록 handle메서드를 구현합니다. nextHandler가 마지막이면 없을 수도 있기때문에 nextHandler가 있는지 체크하는 if문을 추가합니다.

public abstract class RequestHandler {

    private RequestHandler nextHandler;

    public RequestHandler(RequestHandler nextHandler) {
        this.nextHandler = nextHandler;
    }

    public void handle(Request request) {
        if (nextHandler != null) {
            nextHandler.handle(request);
        }
    }
}

이제 RequestHandler를 상속받는 ConcreteHandler들을 구현해보겠습니다. 각 Handler들은 하나의 책임만을 가지고 있습니다.

Request의 본문을 출력하는 PrintRequestHandler

public class PrintRequestHandler extends RequestHandler {

    public PrintRequestHandler(RequestHandler nextHandler) {
        super(nextHandler);
    }

    @Override
    public void handle(Request request) {
        System.out.println(request.getBody());
        super.handle(request);
    }
}

인증처리를하는 AuthRequestHandler

public class AuthRequestHandler extends RequestHandler {

    public AuthRequestHandler(RequestHandler nextHandler) {
        super(nextHandler);
    }

    @Override
    public void handle(Request request) {
        System.out.println("인증이 되었는가?");
        super.handle(request);
    }
}

Logging을 하는 LoggingRequestHandler

public class LoggingRequestHandler extends RequestHandler {

    public LoggingRequestHandler(RequestHandler nextHandler) {
        super(nextHandler);
    }

    @Override
    public void handle(Request request) {
        System.out.println("로깅");
        super.handle(request);
    }
}

생성한 Handler와 ConcreteHandler들을 Client에서 사용해 보겠습니다. RequestHandler를 생성자로 받아서 doWork메서드에서 Request를 받아서 처리하도록 합니다. 그러려면 Chain을 미리 만들어 줘야합니다. AuthRequestHandler -> LogginRequestHandler -> PrintRequestHandler순으로 처리하도록 체인을 생성하고 그 chain을 Client의 생성자에 세팅해서 doWork메서드를 호출해 처리합니다.

public class Client {

    private RequestHandler requestHandler;

    public Client(RequestHandler requestHandler) {
        this.requestHandler = requestHandler;
    }

    public void doWork() {
        Request request = new Request("이번 놀이는 뽑기입니다.");
        requestHandler.handle(request);
    }

    public static void main(String[] args) {
        RequestHandler chain = new AuthRequestHandler(new LoggingRequestHandler(new PrintRequestHandler(null)));
        Client client = new Client(chain);
        client.doWork();
    }
}

실행 결과과 인증 -> 로깅 -> 출력 순서대로 나오는 것을 확인할 수 있습니다.

인증이 되었는가?
로깅
이번 놀이는 뽑기입니다.

3. 결론

책임 연쇄 패턴 (Chain-of-responsibility) 패턴은 클라이언트 코드를 변경하지 않고 새로운 핸들러를 체인에 추가하고 순서를 변경할 수 있습니다. 그리고 체인에 있는 각각의 핸들러는 한가지 책임만을 가지고 자신이 해야하는 일만을 합니다. 하지만 체이닝을 통해 코드의 이동이 많아지기 떄문에 디버깅이 조금 번거롭습니다.

참고자료

profile
백엔드 개발자 입니다

0개의 댓글