책임 연쇄 패턴 (Chain of Responsibility)
- 각각의 책임이 연결된 패턴이다.
- 단일 책임 원칙에서 말하는 그 책임과 비슷하다.
- ex) 어떤 클래스가 변경되어야 한다면 그 이유는 단 한가지 (단일 책임) 여야만 한다.
- 단일 책임 원칙에서 말하는 그 책임과 비슷하다.
- 요청을 보내는 쪽과 요청을 처리하는 쪽 (핸들러) 을 분리하는 패턴이다.
- 일련의 핸들러를 따라 요청을 전달하는 패턴이다.
- 핸들러는 요청을 받으면 자신이 처리할지 다음 핸들러로 넘길지 결정한다.
- 일련의 핸들러를 가지고 있지만 실제요청이 들어오기 전까지는 어떤 핸들러를 써야할지 모를 때 유용하다.
- 적절한 핸들러를 찾거나 체인이 모두 소진될 때까지 체인을 따라 요청이 전달된다.
다이어그램으로 살펴보기
- 클라이언트는 핸들러 체인에 요청을 보내 처리를 요청한다.
- 클라이언트는 자신이 어떤 핸들러에 요청하는지도 모른다.
- 요청을 하는 곳과 요청을 처리하는 곳이 완전히 분리된다.
문제
public static void main(String[] args) {
Request requestLog = new Request("로깅 핸들러가 잘 처리하는 요청");
Request requestAuth = new Request("권한이 있는 유저인지 인증해야 하는 요청");
Request requestValidation = new Request("값 검증이 중요한 요청");
RequestHandler requestHandlerLog = new LoggingRequestHandler();
RequestHandler requestHandlerAuth = new AuthRequestHandler();
RequestHandler requestHandlerValidation = new ValidationRequestHandler();
requestHandlerLog.handler(requestLog);
requestHandlerAuth.handler(requestAuth);
requestHandlerValidation.handler(requestValidation);
}
log
,auth
,validation
에 대한 요청을 각각 만들어두고 각 요청을 처리할 핸들러 3개를 만들었다.- 클라이언트 단에서 매번 핸들러를 새로 인스턴스화 시키는 방식이 아니라 하나의 핸들러로 모든 요청을 알맞게 처리할 순 없을까?
- 그리고 또 생각해볼만한 점은 만일 하나의 요청을 3개의 핸들러에서 각각 처리하게 만들고 싶다면 어떻게 해야 할까?
- 만일 체인을 만든다면 체인의 순서에 대해서도 고려해볼만하다.
- 다음 체인을 갔다가 다시 돌아오도록 설계할 수도 있다.
해결
RequestHandler
: 핸들러 체인에 사용될 추상 클래스 구현
- 이 추상 클래스는
RequestHandler
타입의 객체 하나를 필드로 가져서 다음 핸들러로 넘기는 기본 동작을 가지고 있다.
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);
}
}
}
AuthRequestHandler
: 인증 관련 로직을 처리하는 핸들러 구현
- 바디에 인증 관련된 내용이 있으면 인증 관련 로직을 수행한다.
public class AuthRequestHandler extends RequestHandler{
public AuthRequestHandler(RequestHandler nextHandler) {
super(nextHandler);
}
@Override
public void handle(Request request) {
if (request.getBody().contains("인증")) {
System.out.println("인증이 필요한 요청 발견. 핸들러 작동.");
}
super.handle(request);
}
}
LoggingRequestHandler
: 로깅 관련 로직을 처리하는 핸들러 구현
- 로깅에 관련된 내용이 있으면 로깅 관련 로직을 수행한다.
public class LoggingRequestHandler extends RequestHandler {
public LoggingRequestHandler(RequestHandler nextHandler) {
super(nextHandler);
}
@Override
public void handle(Request request) {
if (request.getBody().contains("로깅")) {
System.out.println("로깅이 필요한 요청 발견. 핸들러 작동.");
}
super.handle(request);
}
}
ValidationRequestHandler
: 검증 관련 로직을 처리하는 핸들러 구현
- 검증에 관련된 내용이 있으면 검증 관련 로직을 수행한다.
public class ValidationRequestHandler extends RequestHandler{
public ValidationRequestHandler(RequestHandler nextHandler) {
super(nextHandler);
}
@Override
public void handle(Request request) {
if (request.getBody().contains("검증")) {
System.out.println("검증이 필요한 요청 발견. 핸들러 작동.");
}
super.handle(request);
}
}
Client
코드 수정
- 아래의 코드를 보면 알 수 있듯 이제 하나의 핸들러 체인에서 모든 요청에 대한 처리가 가능해졌다.
- 하나의 요청에서 모든 핸들러가 작동하게 하는 것도 가능하다.
- 의도적으로 원하는 핸들러만 작동하도록 하는 것도 가능하다.
public class Client {
public static void main(String[] args) {
RequestHandler handlerChain = new AuthRequestHandler(new LoggingRequestHandler(new ValidationRequestHandler(null)));
Request requestLog = new Request("로깅 핸들러가 잘 처리하는 요청");
Request requestAuth = new Request("권한이 있는 유저인지 인증해야 하는 요청");
Request requestValidation = new Request("값 검증이 중요한 요청");
Request requestAll = new Request("로깅 인증 검증 다 필요한 요청");
Request requestLogValidation = new Request("로깅과 검증만 필요한 요청");
System.out.println("------------로깅 요청 결과--------------");
handlerChain.handle(requestLog);
System.out.println("------------인증 요청 결과--------------");
handlerChain.handle(requestAuth);
System.out.println("------------검증 요청 결과--------------");
handlerChain.handle(requestValidation);
System.out.println("------------모든 요청 결과--------------");
handlerChain.handle(requestAll);
System.out.println("------------로깅 검증 요청 결과--------------");
handlerChain.handle(requestLogValidation);
}
}
/*
출력결과:
------------로깅 요청 결과--------------
로깅이 필요한 요청 발견. 핸들러 작동.
------------인증 요청 결과--------------
인증이 필요한 요청 발견. 핸들러 작동.
------------검증 요청 결과--------------
검증이 필요한 요청 발견. 핸들러 작동.
------------모든 요청 결과--------------
인증이 필요한 요청 발견. 핸들러 작동.
로깅이 필요한 요청 발견. 핸들러 작동.
검증이 필요한 요청 발견. 핸들러 작동.
------------로깅 검증 요청 결과--------------
로깅이 필요한 요청 발견. 핸들러 작동.
검증이 필요한 요청 발견. 핸들러 작동.
*/
장점과 단점
장점
- Sender 와 Receiver 가 분리되었다.
- 이 덕분에 핸들러를 더 추가하더라도 클라이언트 코드에 커다란 변화가 일어나지 않는다.
- 이는 Open Closed Principle 과 관련이 있다.
- 유연성이 뛰어나다
- 체인은 런타임에도 추가되거나 삭제될 수 있다.
- 클라이언트 코드가 간결해진다.
- 하나의 핸들러에서 모든 처리가 이루어지기 때문에 전체적인 클라이언트 코드 복잡도가 줄어든다.
- 체인에 있는 핸들러 하나하나씩은 하나씩의 책임만 갖는다.
- Single Responsibility Principle 을 잘 지킨다.
단점
- 연쇄적으로 흘러가다보니 디버깅이 조금 번거로울 수 있다.
- 요청이 몇몇 핸들러를 왔다갔다해야 해서 약간의 퍼포먼스 오버헤드가 있다.
- 요청을 처리할 핸들러가 실제로 존재하는지에 대한 증명이 부족하다.
자바와 스프링에 존재하는 책임 연쇄 패턴
서블릿 필터
- 서블릿에 도달하기 전까지 필터를 거치게 된다.
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("다음 필터 호출 전 할 일");
chain.doFilter(request, response);
System.out.println("다음 필터 후 할 일");
}
스프링 시큐리티 설정
- 스프링 시큐리티 내부 구현을 보면 스프링 시큐리티 설정 자체가 거대한 필터 체인을 설정하는 것이다.
.and().addFilter()
등을 통해서 다른 필터들을 추가할 수 있다.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll().and().addFilter(SomeFilter);
}
}
레퍼런스
반응형
'Java > 자바 디자인 패턴' 카테고리의 다른 글
인터프리터 패턴 (Interpreter Pattern) 이란? (0) | 2023.04.30 |
---|---|
커맨드 패턴 (Command Pattern) 이란? (0) | 2023.04.28 |
프록시 패턴 (Proxy Pattern) 이란? (0) | 2023.04.25 |
플라이웨이트 패턴 (Flyweight Pattern) 이란? (0) | 2023.04.22 |
퍼사드 패턴 (Facade Pattern) 이란? (0) | 2023.04.18 |