[백엔드] Spring Security 첫 걸음 - DelegatingFilterProxy, FilterChainProxy로 Web Context Filter에 Spring Bean Filter 등록하기

2026. 2. 14. 02:30·CS/백엔드

DelegatingFilterProxy

그 전에 그냥 HTTP 요청이 어떻게 처리되냐면..

https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-filters-review

클라이언트로부터 요청이 들어오면 HttpServletRequest, HttpServletResponse 객체가 생성되며, 이 객체는 필터들을 통과하며 조건에 부합하는 요청인지를 검사받게되고, 서블렛과정에서 실제 비즈니스 로직이 수행된다. 스프링에서 Servlet의 인스턴스는 DispatcherServlet이라는 핵심 인스턴스가 담당하며, 클라이언트로부터 들어온 요청을 분석해 적절한 컨트롤러에 인계한다. 그래서 모든 과정이 시작되는 것이다.

 

하나의 서블렛은 하나의 HttpServletRequest나 HttpServletResponse를 처리할 수 있다.

그리고 필터에서는 하위필터로 요청이 내려가는 것을 막고, 필터 스스로가 HttpServletRequest나 HttpServletResponse를 다룰 수 있다. 이 경우 서블렛은 요청을 처리하지 않는다.

 

@Override
public void doFilter(ServletRequest request, ServletResponse response,
		FilterChain chain) throws IOException, ServletException {
	// do something before the rest of the application
	chain.doFilter(request, response); // invoke the rest of the application
	// do something after the rest of the application
}

아무튼 필터와 서블렛을 합쳐서 필터체인이라고 일컫는다. 

필터체인에 들어가는 필터는 모두 Filter 의 인스턴스가되며, 요청을 필터링하는 과정은 doFilter() 메서드를 호출하여 처리할 수 있다. 또한 필터는 상위필터에서 순차적으로 내려가는 구조로 서블렛에 도달하는 구조를 가지고 있기 때문에, 필터의 순서가 중요하다.


스프링은 이런 구조를 가지고있다.

Servlet Container, 그러니까 톰캣서버는 Filter 클래스의 하위 인스턴스를 등록할 수 있다. 하지만, 스프링빈은 알지못한다. 그러니까 톰캣서버는 이게 스프링 빈에 등록된 Filter인지 알지 못한다.

 

따라서 스프링이 따로 필터를 만들어서 직접 톰캣서버에 등록해도되지만, 우선은 필터는 순서가 중요한 편이고, 모든 스프링 필터를 한 번에 묶어서 관리할 필요가 있으므로 필터들을 묶어서 하나의 FilterChainProxy를 만든다. 따라서 DelegatingFilterProxy는 이 FilterChainProxy를 톰캣서버에 전달해주는 역할을한다.

요로코롬생겼다.

필터체인에 DelegatingFilterProxy가 등록한, 스프링에서 만들어낸 필터가 들어간다. 그러면 클라이언트로부터 요청이 들어오면 필터를 하나씩 처리하며, 스프링 필터의 순서가 되면 DelegatingFilterProxy가 등록한 필터들을 처리한 후에 다시 원래 필터로 돌아와 서블렛까지 요청을 전달한다.

 

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
	Filter delegate = getFilterBean(someBeanName);
	delegate.doFilter(request, response);
}

 

DelegatingFilterProxy의 doFilter() 메서드 의사코드 여기서는 someBeanName이라는 이름을 가진 FilterChainProxy로 전달한다.

 

또다른 장점으로는 Filter와 Spring Bean의 생명주기의 차이를 보완할 수 있다는 것이 있는데, FilterChain은 서버가 시작되면 Filter 인스턴스를 생성해 필터체인에 필터를 등록하는 반면, Spring Bean은 ContextLoaderListener 가 실행된 후, ApplicationContext를 초기화시킨 후에 사용할 수 있다.

 

따라서 Spring Bean이 생기기 전에 Filter에서 스프링이 관리하는 필터를 요구할 수 있다. 그래서 스프링에서 관리하는 필터를 스프링이 그대로 필터체인에 등록하면 다른 생명주기로 인한 문제가 발생할 수 있다.

 

하지만 DelegatingFilterProxy는 이런 생명주기차이를 보완하는데, 스프링에서 관리하는 필터를 찾는 것을 지연시키기 때문에 ContextLoaderListener가 실행된 후에 필터를 스프링 컨테이너가 구동된 후에 가져오게할 수 있다.


FilterBeanProxy

내가 한땀한땀 만진 SecurityConfig를 보자면..

@Bean
    public SecurityFilterChain filterChain (HttpSecurity http) throws Exception {

        ...각종 설정들...
                
        return http.build();
    }

마지막으로 HttpSecurity의 build 메서드를 부르게된다.

 

public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
		implements SecurityBuilder<DefaultSecurityFilterChain>, HttpSecurityBuilder<HttpSecurity> { ... }

HttpSecurity의 build 메서드는 HttpSecurity가 구현한 SecurityBuilder<DefualtSecurityFilterChain>의 상위클래스인 AbstractSecurityBuilder에서 가져온 메서드로, 제너릭 타입을 반환한다. 따라서 해당 메서드가 실행되면 SecurityFilterChain이 생성된다.

 

여기서 SecurityFilterChain이 FilterBeanProxy이고, 이것을 DelegatingFilterProxy가 필터로 등록시킨다.

 

SecurityFilterChain

 

그래서 FilterBeanProxy의 예시인 SecurityFilterChain에 대해서 알아보면.. Spring Security가 만들어낸 Filter들을 하나로 엮는 역할을한다.

 

@Bean
    public SecurityFilterChain filterChain (HttpSecurity http) throws Exception {

        http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/",
                                "/css/**",
                                "/js/**",
                                "/images/**",
                                "/favicon.ico/**",
                                "/h2-console/**",
                                "/signup/**",
                                "/oauth2/**").permitAll()
                        .requestMatchers(HttpMethod.GET, "/review/**").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2Login(oauth2 -> oauth2
                        .defaultSuccessUrl("/")
                        .successHandler((request, response, authentication) -> {
                            oauth2SuccessHandler.onAuthenticationSuccess(request, response, authentication);
                        })
                        .failureUrl("/login?error=true")
                )
                .addFilterBefore(new JWTAuthorizationFilter(jwtProvider, memberRepository),
                        UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(ex -> ex
                        .authenticationEntryPoint(new JWTAuthorizationEntryPoint()))
                .sessionManagement((session)-> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .csrf(AbstractHttpConfigurer::disable)
                .headers(headers -> headers
                        .frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
                )
                .logout(logout -> logout
                        .logoutSuccessUrl("/")
                );

        return http.build();
    }

이렇게 만진 요소 하나하나가 필터 역할을한다.

 

이렇게 필터를 묶어서 FilterBeanProxy로 관리하면 좋은 점은

1. 모든 스프링 Security 기능의 시작점이다. 모든 필터를 따로 톰캣서버의 필터로 등록했다면 디버깅 할 때 모든 필터들을 뒤져가야하지만 FilteBeanProxy를 사용하면 디버깅 시작점을 FilterBeanProxy로 설정하면 되는 등, 하나의 기능구분선이 된다.

 

2. FilterBeanProxy의 하위필터들이 필터링을 시작하기 전후로 매우 중요한 보안처리를 할 수 있다. 예를들면 Spring Security는 요청 과정에서 요청 과정에 대한 정보를 담은 SecurityContext를 ThreadLocal에 저장하고 요청이 끝나면 ThreadLocal을 재사용하기 위해서 SecurityContext를 한 번 비운다. 이 과정은 메모리누수와 SecurityContext의 정보가 꼬이는 것을 방지하는 중요한 과정이며, 비우는 과정은 모든 필터들을 관리하고있는 SecurityFilterChain이 담당한다. 또 모든 필터들을 관리하고 있으므로 방화벽 기능도 사용할 수 있다.

 

3. 톰캣서버의 필터들은 URL에 의해서만 활성화될 수 있지만, FilterChainProxy는 HttpRequest 객체 내의 어떠한 정보든, RequestMatcher 인터페이스를 통해서 활성화될 수 있다. 즉, FilterChainProxy를 활용하면 HttpRequest의 정보에 따라서 활성화 여부를 결정할 수 있다는 뜻이다. 만약 FilterChainProxy 없이 DelegatingBeanProxy가 그대로 서블렛필터에 스프링 필터를 등록하거나, 아니면 직접 스프링 필터를 등록한다면 이 기능을 사용할 수 없을 것이다. (DelegatingBeanProxy는 단순히 스프링과 톰캣서버의 연결기능만 수행한다.)

 

이렇게 FilterChainProxy에서 SecurityFilterChain은 여러 개가 등록될 수 있으며, 요청이 들어왔을 때 가장먼저 매칭되는 SecurityFilterChain이 실행된다. 뒤에것은 무시.

 

중요한 것 하나는 다른 SecurityFilterChain이라면 내부의 Filter들의 개수가 달라도되고, 구성요소가 달라도된다. 즉, 아예 별개로 구성할 수 있다.

 

지금까지는 내가 SecurityConfig를 하나만 등록해서 사용했지만, 사실은 여러 개를 등록해도된다. 만약 내가 /api의 하위 주소에는 JWT로그인 방식을 사용하고, /apis의 하위주소에는 세션로그인 방식을 사용하고 싶다면, requestMatcher에 각기 다른 Filter를 작성해주면 된다.

 

https://docs.spring.io/spring-security/reference/api/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.html

 

HttpSecurity (spring-security-docs 7.0.3 API)

Configures a SAML 2.0 metadata endpoint that presents relying party configurations in an payload. By default, the endpoints are /saml2/metadata and /saml2/metadata/{registrationId} though note that also /saml2/service-provider-metadata/{registrationId} is

docs.spring.io

Security Config에 들어가는 메서드들에 대한 내용은 위 문서를 보면된다. 솔직히 다 외우는 것은 좀 옳지 못한 행동인 것 같고, 두고두고 찾아볼 수 있도록하자

 

.addFilterBefore(new JWTAuthorizationFilter(jwtProvider, memberRepository),
                        UsernamePasswordAuthenticationFilter.class)

커스텀 Filter를 특정 Filter 앞에 넣고싶다면 이렇게 구성하면된다.

 

https://dev-dx2d2y-log.tistory.com/216

 

[골든리포트!] 6) 필터체인에서 JWT필터 인증 처리하기

몇 가지 수정사항이 생겼다.GDG 플젝트랙에서는 모든 경로를 permitAll() 해주고 인증이 필요한 요청에 대해 헤더에 있는 JWT 토큰을 검사하는 방식으로 구현했다. 물론 이 방식은 딱히 좋은 편이 아

dev-dx2d2y-log.tistory.com

커스텀필터 구현은 이전에 했으므로 패스

공식문서에서 친절히 필터를 추가하는 방법이 나와있다.

 

또한, 커스텀필터는 Filter 클래스의 인스턴스인데, 모든 Filter 클래스의 인스턴스는 자동으로 스프링 컨테이너에 등록된다. 따라서 커스텀필터에 @Component와 같은 어노테이션을 사용하면 스프링 컨테이너에 두 번 커스텀필터가 추가된다.

 

이를 막기 위해서는 @Component를 사용하지 말거나, 커스텀필터를 반드시 빈으로 등록해야할 경우 (DI 등 반드시 스프링의 지원을 받아야하는 경우) FilterRegistration<필터명>을 재정의해 빈으로 등록해야한다.

@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
    FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
    registration.setEnabled(false);
    return registration;
}

이렇게. 여기서는 커스텀필터를 TenantFilter로 설정했는데, TenantFilter에서 setEnabled를 false로 설정해 해당 필터를 컨테이너에 등록하지 않고 @Component 어노테이션으로 컨테이너에 등록시킨다.


예외처리

Filter 과정에서 에러는 AccessDeniedException과 AuthenticationExcpetion이 발생한다. (※커스텀 필터에서도 두 에러만 발생시켜야 제대로 Handler 클래스에서 잡아낼 수 있다.)

 

이 두 에러를 잡아내는 클래스는 ExceptionTranslationFilter이며, SecurityFilterChain에 등록되어 있다.

인증과 관련된 기능들은 모두 ExceptionTranslationFilter의 앞부분에서, 인가와 관련된 기능들은 모두 뒷부분에서 다룬다. (Interceptor가 여기를 담당한다.)

 

따라서 ExceptionTranslationFilter는 인가 과정, 인터셉터에서 발생한 에러를 담당하게된다. (만약 ExceptionTranslationFilter가 동작하기 이전의 필터에서 에러가 발생하면 그 필터가 자체적으로 에러를 처리한다.) 기본적으로는 doFilter() 메서드를 통해 후속 필터들을 호출한다. 이 doFilter() 메서드에 try-catch 문으로 감싸져있어 에러를 탐지한다.

 

만약 AuthenticationException 에러가 발생하면 SecurityContext를 지우고, HttpServletRequest를 저장한다. 그리고 인증이 되지 않았으므로 인증과정을 수행한다. 이 과정은 AuthenticationEntryPoint에서 담당한다.

 

만약 AccessDenied 에러가 발생했다면 AccessDeniedHandler가 즉시 호출된다.

try {
	filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
	if (!authenticated || ex instanceof AuthenticationException) {
		startAuthentication();
	} else {
		accessDenied();
	}
}

의사코드


//import 생략

@Configureation
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http_ thorws Excpetion{
        http
        .authorizeRequests()
          .antMatchers("/design", "/orders")
            .access("hasRole('ROLE_USER')")
          .antMatchers("/", "/**").access("permitAll")
        
        .and()
          .formLogin()
            .loginPage("/login")
        
        .and()
          .logout()
            .logoutSuccessUrl("/")
        
        .and()
          .csrf()
        ;
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth)
        throws Exception {
    auth
      .userDetailsService(userDetailsService)  //저서 내부에서 사용되는 클래스. 여기서는 생략
      .passwordEncoder(encoder());  //저서 내부에서 사용되는 클래스. 여기서는 생략
    }
}

SpringSecurity를 설정하는 방법이 스프링 5.7 이후로 크게 달라졌는데, 이전 방식은 WebSecurityConfigureAdapter를 상속받아서 Security Config를 만졌다.

 

configure 메서드를 오버라이딩 및 오버로딩하여 설정을 만졌다. 현재는 쓰이지 않는다.

WebSecurityConfigureAdapter도 필터체인을 만들긴했으나, 언제 필터체인이 만들어지는지도 몰랐고 (현행방식은 메서드가 호출되면 만들어진다) configure 메서드의 호출될 때도 다른 등... 여러 블랙박스 기능들이 많았다. 따라서 어느 순서대로 config를 동작시킬지, @Order(숫자) 어노테이션이 필수였다.

 

이 과정이 너무나도 복잡하고, 헷갈리기 때문에 현재는 Deprecated 되었고 앞서 봤던대로 곧바로 SecurityFilterChain을 형성한다. 그래서 언제 SecurityFilterChain이 생겨나는지 명호가하게 알 수 있기도하고 configure() 메서드를 굳이 여러 번 호출해 순서가 헷갈릴 염려도 없게하기 위해서 현재는 곧바로 SecurityFilterChain을 만들어내는 편이다. 다만 다중체인환경에서는 @Order(숫자) 어노테이션이 여전히 필요하다. 없어도 되는데, 없으면 스프링이 알아서 빈으로 등록하기 때문에 순서가 헷갈릴 수 있다.

 

과거 강의나 저서, 블로그를 찾아보면 SecurityConfig 부분에 WebSecurityConfigureAdapter 를 상속받는 부분이 있는데, 이러한 방식을 사용하면 안된다.

'CS > 백엔드' 카테고리의 다른 글

[백엔드] 인증요청 시의 SecurityFilterChain의 동작과정  (0) 2026.02.18
[백엔드] 로그인 시의 SecurityFilterChain의 동작과정  (0) 2026.02.16
[백엔드] OAuth는 어떻게 진행되는가?  (0) 2026.02.06
[BckEnd] 스프링없이 순수JDK로 간단한 개발해보기  (0) 2026.01.04
CSRF 공격이란 무엇인가?  (0) 2025.11.25
'CS/백엔드' 카테고리의 다른 글
  • [백엔드] 인증요청 시의 SecurityFilterChain의 동작과정
  • [백엔드] 로그인 시의 SecurityFilterChain의 동작과정
  • [백엔드] OAuth는 어떻게 진행되는가?
  • [BckEnd] 스프링없이 순수JDK로 간단한 개발해보기
Radiata
Radiata
개발을 합니다.
  • Radiata
    DDD
    Radiata
  • 전체
    오늘
    어제
    • 분류 전체보기 (211) N
      • 신년사 (3)
        • 2025년 (2)
        • 2026년 (1)
      • CS (59) N
        • JVM (12)
        • 백엔드 (20) N
        • 언어구현 (1)
        • 객체지향 (1)
        • 논리회로 (5)
        • 컴퓨터구조 (9)
        • 데이터베이스 (1)
        • 컴퓨터 네트워크 (10)
      • 언어공부 (64)
        • Java | Kotlin (48)
        • JavaScript | TypeScript (9)
        • C | C++ (6)
      • 개인 프로젝트 (11)
        • [2025] Happy2SendingMails (3)
        • [2026] 골든리포트! (8)
        • [2026] 순수자바로 개발하기 (0)
        • 기타 이것저것 (0)
      • 팀 프로젝트 (29)
        • [2025][GDG]홍대 맛집 아카이빙 프로젝트 (29)
      • 알고리즘 (13)
        • 백준풀이기록 (11)
      • 놀이터 (0)
      • 에러 수정일지 (2)
      • 고찰 (24)
        • CEOS 23기 회고록 (2)
  • 블로그 메뉴

    • CS
    • 언어공부
    • 개인 프로젝트
    • 팀 프로젝트
    • 알고리즘
    • 고찰
    • 신년사
    • 컬러잇 개발블로그
  • 링크

    • 컬러잇 개발블로그
  • 공지사항

  • 인기 글

  • 태그

    144
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Radiata
[백엔드] Spring Security 첫 걸음 - DelegatingFilterProxy, FilterChainProxy로 Web Context Filter에 Spring Bean Filter 등록하기
상단으로

티스토리툴바