Java/Design Pattern

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

민돌v 2023. 4. 11. 10:37
(인프런) 코딩으로 학습하는 GoF의 디자인 패턴 - 백기선, 강의를 보고 정리한 글입니다.
코드는 GitHub 에 있습니다

 

#1. 객체 생성 관련 패턴

#2. 구조 관련 패턴

 

#3. 행동 관련 패턴

 

 

 


✔️ 책임 연쇄 패턴이란 (Chain of Responsibility patterns)

책임 연쇄 패턴에서의 책임이란, 객체지향 5가지 원칙 중 - 단일 책임 원칙에서 말하는 책임과 동일한 의미를 내포합니다.
  • 👏🏻 책임 연쇄 패턴이란, 클라이언트의 요청을 처리할 수 있는 책임을 가지는 처리객체를 집합(Chain)으로 만들어 부여함으로써 결합을 느슨하게 만들기 위한 디자인 패턴입니다.
  • 일반적으로 요청을 처리할 수 있는 객체를 찾을 떄 까지 집합 안에서 요청을 전달합니다.
  • 요청이 들어오면, 요청을 처리하는 책임을 가지는 handler가 체인처럼 연결되어서 각 요청에대한 책임을 처리합니다.
  • 따라서 클라이언트는 내부의 구체적인 구현 코드를 알지 못하도록 하는 목적또한 가지는 패턴입니다. 

 

 


✔️ 책임 연쇄 패턴 예시

1) 책임 연쇄 패턴 적용 전

  • 클라이언트가 어떤 요청을 할때, 요청을 처리하기 전에 이 클라이언트에대해 인증에 대한 기능을 해야한다고 가정합니다.
public class Client {

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

        //클라이언트가 요청
        requestHandler.handler(request);
    }
}

@Getter
public class Request {

    private final String time;
    private final String body;

    public Request(String body) {
        this.body = body;
        this.time = LocalDateTime.now().toString();
    }
}

 

  • 요청을 처리하는 객체의 기능안에 "특정한 인가의 기능" 을 추가해도 되지만 이렇게 하면 → 단일책임 원칙에 위배됩니다.
@Value
public class RequestHandler {

    public void handler(Request request) {
        //무언가 인증이 필요해서 코드에 넣을 때 → 단일책임에 위배
        System.out.println("이 유저는 인증이 되었나?");

        System.out.println("request = " + request.getBody());
    }
}

AuthRequestHandler.class

  • 구체적인 기능에대한 책임을 하위 클래스로 추출하면 단일책임 원칙에 위배되지 않게 수정할 수 있습니다.
public class RequestHandler {

    public void handler(Request request) {

        System.out.println("request = " + request.getBody());
    }
}

public class AuthRequestHandler extends RequestHandler {

    public void handler(Request request) {
        //무언가 인증이 필요해서 코드에 넣을 때 → 단일책임에 위배
        System.out.println("이 유저는 인증이 되었나?");
        super.handler(request);
    }
}

Client.class

  • 하지만 클라이언트에서 구체적인 기능을 가지는 클래스를 호출해야합니다.
public class Client {

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

        //클라이언트가 구체적인 클래스를 선택
        RequestHandler requestHandler = new AuthRequestHandler();

        //클라이언트가 요청
        requestHandler.handler(request);
    }
}

LoggingHandler.class

  • 또한 로깅같은 특정한 기능을 선택하고 싶을때, 또다시 클라이언트의 코드가 변경되어야하고
  • "인증" or "로깅" 2가지 기능을 동시에 처리하고 싶을때 새로운 클래스가 생성되는 등 복잡한 처리를 진행해야합니다.
public class LoggingRequestHandler extends RequestHandler{

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

Client.class

public class Client {

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

        //클라이언트가 구체적인 클래스를 선택
        RequestHandler requestHandler = new AuthRequestHandler();
        //로깅을 할려면 또다른 클래스를 선택
        //RequestHandler requestHandler = new LoggingRequestHandler();

        //→ 하지만 2가지 기능을 동시에 하고싶을때 복잡해짐

        //클라이언트가 요청
        requestHandler.handler(request);
    }
}

 

📌 이러한 문제점을 책임연쇄 패턴으로 클라이언트는 구체적인 내부 구현체를 알지 못하게 하여 변경을 줄이고

📌 각 책임을 가지는 객체들을 연결하여 다중 기능을 처리할 수 있도록 개선할 수 있습니다.


 

2) 책임 연쇄 패턴 적용 후

  • 책임 연쇄 패턴의 한 구조입니다.
  • 먼저 각 행위를 캡슐화하고 → 캡슐화한 객체를 추상화하고 다음 행위를 호출하기 위한 추상클래스를 정의하겠습니다.
public abstract class RequestHandler {

    //다음 핸들러를 호출하도록 이어지게
    private RequestHandler nextHandler;

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

    public void handler(Request request) {
        //마지막이 아닐경우
        if (nextHandler != null) {
            nextHandler.handler(request);
        }
    }
}

 

  • 그리고 특정기능에 대한 책임을 가지는 구체적인 클래스를 캡슐화하여 구현합니다.
  • 핸들러(하위 클래스) 는 본인의 책임이 끝나면 부모의 handler 호출 함수를 호출합니다.
public class AuthRequestHandler extends RequestHandler {

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

    @Override
    public void handler(Request request) {
        System.out.println("인증 ,,,,중... 완료!");
        super.handler(request);
    }
}

public class LoggingRequestHandler extends RequestHandler{

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

public class PrintRequestHandler extends RequestHandler {

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

    @Override
    public void handler(Request request) {
        System.out.println("request.getBody() = " + request.getBody());
        super.handler(request);
    }
}

 

  • 그런다음 클라이언트의 변경을 최소화하기 위해 Handler 를 외부로부터 주입받고
  • 외부에서는 각 책임을 연결한 Handler를 Client 에게 주입합니다.
public class Client {

    private RequestHandler requestHandler;

    //요청을 처리할 Handler 를 주입받을
    public Client(RequestHandler requestHandler) {
        this.requestHandler = requestHandler;
    }

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

    public static void main(String[] args) {
        //각 책임이 연결되게
        RequestHandler chain = new AuthRequestHandler(new LoggingRequestHandler(new PrintRequestHandler(null)));

        Client client = new Client(chain);
    }
}

 

  • 👏🏻이렇게 되면 각 책임에대한 기능을 클라이언트에게 주입하는 쪽에서 부분적으로 커스텀할 수 있고
  • 클라이언트는 내부적인 구현을 전혀 알지 못하게 됩니다.

 


✔️ 책임 연쇄 패턴 장단점

장점

  • 결합도를 낮추며, 요청의 발신자와 수신자를 분리시킬 수 있습니다.
  • 클라이언트는 처리객체의 집합 내부의 구조를 알 필요가 없습니다.
  • 집합 내의 처리 순서를 변경하거나 처리객체를 추가 또는 삭제할 수 있어 유연성이 향상됩니다.
  • 새로운 요청에 대한 처리객체 생성이 매우 편리해집니다.

단점

  • 충분한 디버깅을 거치지 않았을 경우 집합 내부에서 사이클이 발생할 수 있습니다.
  • 디버깅 및 테스트가 쉽지 않습니다.

 

 

이렇게 해서 디자인 패턴의 행위 패턴 중 하나인 "책임 연쇄 패턴"에 대해서도 공부를 해보았습니다

Spring 에서는 Spring Security 의 인증, 인가 혹은 Dispathcher Servlet 쪽에서 HTTP Request 요청을 처리하는 부분에서 이러한 형태의 패턴을 띄우고있었던거 같습니다

자주 사용되는 패턴인만큼 한번 알아두니까 좋네요! 

 

끝!