Design Pattern

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

꾸준함. 2024. 6. 29. 21:29

책임 연쇄 패턴

  • 요청을 처리할 수 있는 여러 개의 처리 객체를 연결하여 요청을 처리할 수 있는 객체가 요청을 처리할 때까지 차례로 전달하는 행동 디자인 패턴
  • 해당 패턴은 다음과 같은 상황에서 유용하게 사용됨
    • 여러 객체가 요청을 처리해야 할 때: 요청을 처리할 수 있는 객체가 여러 개 있고, 각 객체가 자신이 처리할 수 없는 요청은 다음 객체로 전달하는 경우
    • 객체 간의 결합도를 낮춰야 할 때: 요청을 보내는 객체와 요청을 처리하는 객체 간의 결합도를 낮춰서 더 유연한 코드를 작성하고자 할 때
    • 동적으로 요청 처리 객체를 변경할 수 있어야 할 때: 실행 중에 요청 처리 객체를 동적으로 변경할 수 있도록 해야 할 때

 

https://medium.com/geekculture/understanding-the-chain-of-responsibility-pattern-d729ef84621c

 

주요 구성 요소

 

1. Handler

  • 요청을 처리하는 인터페이스 혹은 추상 클래스
  • 다음 Handler를 가리키는 참조를 가지고 있음

 

2. Concrete Handler

  • Handler를 구현하는 구체적인 클래스
  • 요청을 처리할 수 있으면 처리하고, 그렇지 않으면 다음 Handler에게 요청을 전달

 

3. Client

  • 요청을 생성하고 체인의 첫 번째 Handler에게 요청을 전달하는 객체

 

책임 연쇄 패턴 동작 과정

  • 클라이언트는 요청을 생성하고 체인의 첫 번째 처리자에게 전달
  • 각 처리자는 요청을 처리할 수 있는지 검사
    • 처리할 수 있으면 요청을 처리하고, 체인은 종료
    • 처리할 수 없으면 요청을 다음 처리자에게 전달

 

  • 체인의 끝까지 요청이 전달되며, 끝까지 요청을 처리할 수 있는 객체가 없다면 요청은 처리되지 않음

 

책임연쇄 패턴 구현 예시

 

1. Handler 추상 클래스

 

public abstract class Handler {
protected Handler nextHandler;
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
public abstract void handleRequest(String request);
}
view raw .java hosted with ❤ by GitHub

 

2. ConcreteHandler


public class ConcreteHandler1 extends Handler {
@Override
public void handleRequest(String request) {
if (request.equals("요청1")) {
System.out.println("ConcreteHandler1이 요청을 처리합니다.");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
public class ConcreteHandler2 extends Handler {
@Override
public void handleRequest(String request) {
if (request.equals("요청2")) {
System.out.println("ConcreteHandler2가 요청을 처리합니다.");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
view raw .java hosted with ❤ by GitHub

 

3. 클라이언트 코드

 

public class Client {
public static void main(String[] args) {
Handler handler1 = new ConcreteHandler1();
Handler handler2 = new ConcreteHandler2();
handler1.setNextHandler(handler2);
handler1.handleRequest("요청1");
handler1.handleRequest("요청2");
handler1.handleRequest("알 수 없는 요청");
}
}
view raw .java hosted with ❤ by GitHub

 

책임 연쇄 패턴 장단점

 

장점

  • 클라이언트 코드를 변경하지 않고 새로운 핸들러를 체인에 추가 가능 (SOLID의 OCP 원칙)
    • 각각의 체인은 자신이 해야 하는 일만 수행할 수 있음 (SOLID의 SRP 원칙)
    • 체인은 다양한 방법으로 구성 가능

 

단점

  • 디버깅을 위해서는 핸들러를 타고타고 들어가야 하기 때문에 트러블 슈팅이 다소 어려울 수 있음

 

실무에서 쓰이는 책임 연쇄 패턴

 

1. 자바의 서블릿 필터

  • 서블릿 필터(Servlet Filter)는 자바 웹 애플리케이션에서 클라이언트의 요청과 서버의 응답을 인터셉트하여 전처리 또는 후처리 작업을 수행하는 기능을 제공
  • 서블릿 필터는 책임 연쇄 패턴을 적용한 대표적인 예로, 여러 필터가 체인 형태로 연결되어 순차적으로 요청을 처리

 

서블릿 필터 동작 원리

  • 클라이언트의 요청이 필터 체인으로 전달됨: 클라이언트가 서블릿이나 JSP와 같은 웹 리소스에 요청을 보내면, 이 요청은 필터 체인(Filter Chain)으로 전달
  • 필터 체인이 요청을 처리함: 필터 체인은 등록된 필터들을 순차적으로 호출하며 요청을 처리하며 각 필터는 요청을 전처리하거나 후처리 할 수 있음
  • 서블릿 또는 JSP가 요청을 처리함: 필터 체인을 통해 모든 필터가 요청을 처리한 후, 최종적으로 서블릿이나 JSP가 요청을 처리
  • 응답이 필터 체인으로 전달됨: 서블릿이나 JSP가 응답을 생성한 후, 이 응답은 다시 필터 체인을 통해 클라이언트로 전달하며 각 필터는 응답을 후처리 할 수 있음


import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
public class LoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 초기화 작업
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("LoggingFilter: Request received");
chain.doFilter(request, response);
System.out.println("LoggingFilter: Response sent");
}
@Override
public void destroy() {
// 정리 작업
}
}
view raw .java hosted with ❤ by GitHub

 

2. 스프링 시큐리티 필터

  • 스프링 시큐리티는 여러 필터들을 체인 형태로 구성하여 요청과 응답을 처리하며, 이는 책임 연쇄 패턴을 적용한 대표적인 사례
  • 스프링 시큐리티는 여러 개의 보안 필터들을 FilterChainProxy라는 하나의 메인 필터로 묶어 관리하며 FilterChainProxy는 클라이언트의 요청이 들어올 때 등록된 보안 필터들을 순차적으로 실행

 

스프링 시큐리티 필터 주요 구성 요소

  • FilterChainProxy: 스프링 시큐리티의 중심 필터로, 여러 보안 필터를 체인 형태로 관리하고 실행
  • SecurityFilterChain: 여러 보안 필터의 집합으로, 특정 요청 패턴에 대해 적용될 필터들을 정의
  • Security Filters: 각 보안 필터는 특정 보안 기능을 담당
    • ex) 인증, 권한 부여, 세션 관리 등을 수행

 

스프링 시큐리티 동작 과정

  • 클라이언트의 요청이 FilterChainProxy로 전달
  • FilterChainProxy는 요청에 맞는 SecurityFilterChain을 지정
  • 지정된 SecurityFilterChain에 정의된 보안 필터들이 순차적으로 실행
  • 각 필터는 요청을 처리하고, 필요에 따라 다음 필터로 요청을 전달
  • 모든 필터가 요청을 처리한 후, 최종적으로 서블릿 또는 다른 웹 리소스가 요청을 처리


@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/api/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(formLogin ->
formLogin
.loginPage("/login")
.permitAll()
)
.logout(logout ->
logout
.permitAll()
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
var user = User.withUsername("user")
.password("{noop}password") // {noop}은 비밀번호를 인코딩하지 않음을 나타냄
.roles("USER")
.build();
var admin = User.withUsername("admin")
.password("{noop}admin")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
view raw .java hosted with ❤ by GitHub

 

참고

코딩으로 학습하는 GoF의 디자인 패턴 - 백기선 강사님

 

반응형