디자인 패턴

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

개발정리 2022. 8. 2. 18:27

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


요청을 보내는 쪽(sender)과 요청을 처리하는(receiver) 쪽을 분리하는 패턴

  • 핸들러 체인을 사용해서 요청을 처리한다.

 

클라이언트로부터의 요청을 처리할 수 있는 처리 객체를 Chain 으로 만들어 결합을 느슨하게 하기 위해 만들어진 패턴.

일반적으로 요청을 처리할 수 있는 객체를 찾을때 까지 집한 안에서 요청을 전달한다.

 

 

 

 

책임 연쇄 패턴 적용 전


클라이언트가 특정 핸들러를 사용할 때, 이 클라이언트가 해당 핸들러를 사용할 수 있는 사용자 인지 인증해야 하는 로직이 있다고 가정해보자. 클라이언트 코드는 아래와 같다.

 

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

 

요청(Request)과 요청 핸들러(Request Handler) 코드는 아래와 같다.

public class Request {
  String body;

  public Request(String body) {
    this.body = body;
  }

  public String getBody() {
    return body;
  }
}
public class RequestHandler {
  public void handler(Request request) {
    // 인증 확인 로직
    // code 생략.. 
    
    // 출력 로직
    System.out.println(request.getBody());
  }
}

 

Request Handler 는 단순히 Request 의 body 를 출력해준다.

 

문제점

Request Handler 는 현재 하나의 핸들러가 두가지 역할을 진행하고 있다. (단일 책임 원칙 위배)

  • 인증 확인 로직
  • 출력 로직 

 

 

이전까지 학습한 패턴을 적용해보자.

책임을 분리하기 위해 인증을 하는 새로운 핸들러를 만들고, Request Handler 를 상속한다.

public class AuthRequestHandler extends RequestHandler{
  public void handler(Request request) {
    // 인증 확인 로직
    // code 생략..
    
    super.handler(request);
  }
}
public class RequestHandler {
    public void handler(Request request) {
      System.out.println(request.getBody());
    }
}

 

AuthRequestHandler 의 handler 메소느는 인증 관련 로직을 처리한 뒤, super 로 원래의 handler 로직을 RequestHandler 에게 맡기면 단일 책임 원칙을 지킬 수 있다.

 

위 와 같이 각각의 책임을 분리하는것이 좋은 방법 같지만 여전히 문제가 발생한다.

 

문제점 1. 

클라이언트는 구현체를 사용하게 된다. (DIP 의존관계 역전 원칙 위배)

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

 

문제점 2.

logging 과 같은 새로운 로직이 추가 될 경우 이는 어떻게 적용시킬 것인가

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

 

 

 

책임 연쇄 패턴 적용 후


책임 연쇄 패턴을 적용하여 클라이언트는 구체적으로 어떤 핸들러를 사용해야하는지 몰라도 기존의 핸들러 들이 수행되도록 적용해보자.

 

 

위 클래스 다이어그램의 Handler 추상타입 클래스를 만들자.

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 의 handle 메소드를 보면 nextHandler 가 있으면 nextHandler 의 handle 을 실행하도록 구현되어 있다.

이제 각 핸들러들은 위 RequestHandler 를 상속받아 작식들의 로직을 구현하면 된다.

 

AuthRequestHandler

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

  @Override
  public void handle(Request request) {
    System.out.println("요청자의 인증을 확인합니다.");

    if (!request.getBody().split(":")[1].equals("인증OK")) {
      System.out.println("인증이 완료된 사용자가 아닙니다. 종료합니다.");
    }
    else {
      super.handle(request);  
    }
  }
}

 

LoggingRequestHandler

public class LoggingRequestHandler extends RequestHandler{

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

  @Override
  public void handle(Request request) {
    System.out.println("요청을 로깅합니다.");
    super.handle(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);
  }
}

 

Client

public class Client {

  private RequestHandler requestHandler;

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

  // 어떤 핸들러를 사용하는지 구체적으로 알 필요가 없다.
  public void doWork() {
    Request request = new Request("요청 부탁해요.:인증OK");
    requestHandler.handle(request);
  }

  // 외부에서 chain 을 조립한다.
  public static void main(String[] args) {
    AuthRequestHandler chain = new AuthRequestHandler(
        new LoggingRequestHandler(new PrintRequestHandler(null)));
    Client client = new Client(chain);
    client.doWork();
  }
}
//결과
요청자의 인증을 확인합니다.
요청을 로깅합니다.
요청 부탁해요.:인증OK
public void doWork() {
  Request request = new Request("요청 부탁해요.:인증NO");
  requestHandler.handle(request);
}
//결과
요청자의 인증을 확인합니다.
인증이 완료된 사용자가 아닙니다. 종료합니다.

 

 

 

장점과 단점


장점

  • 클라이언트 코드를 변경하지 않고, 새로운 핸들러를 체인에 추가할 수 있고, 순서도 변경할 수 있다. (OCP)
  • 각 핸들러들은 본인의 역할만을 수행하게 된다. (SRP)
  • 체인을 구조적으로 다양하게 구성할 수 있다.

 

단점

  • 다양한 필터들을 거치다 보니 디버깅이 쉽지 않다.

 

 

 

실무에선 어떻게 쓰이나?


  • 자바의 서블릿
    • 사용자가 요청시 서블릿에 접근하기 전에 다양한 필터를 거치도록 할 수 있다.
  • 스프링 시큐리티 
    • Security 필터에 대한 설정을 할 수 있다. 
    • ex. http.authorizeRequest().anyRequest().permitAll().and();

 

 

 

참고


 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com