[본캠프] 트러블슈팅

윤영범·2026년 4월 29일

발생과정

Spring Security에서 예외 처리는 다음과 같은 아키텍처로 이루워진다

  1. FilterChain
    Spring Security는 FilterChain으로 구성되어 있으며, 각 필터는 요청을 처리하거나 다음 필터로 전달한다. 예외가 발생하면 예외 처리 필터가 작동한다.
  2. ExceptionTranslationFilter
    해당 필터는 예외를 Spring Security 예외로 변환하고, 적절한 AuthenticationEntryPoint 또는 AccessDeniedHandler를 호출한다. 이 필터는 이 둘을 주입받아 사용한다.
  3. AuthenticationEntryPoint(인증에러)
    AuthenticationEntryPoint는 인증되지 않은 요청에 대한 처리를 담당한다. 일반적으로 로그인 페이지로 리디렉션 하거나 401 Unauthorized 응답을 반환한다
  4. AccessDeniedHandler(인가에러)
    AccessDeniedHandler는 인가되지 않은 요청(권한 부족)에 대한 처리를 담당한다 일반적으로 403 Forbidden 응답을 반환한다

Spring Security의 예외처리흐름

-> 요청이들어온다
   ->FilterChain에서 예외가 발생한다
     ->ExceptionTranslationFilter가 예외를 캐치하고, Spring Security 예외로 전환
       -> AuthenticationEntryPoint,AccessDeniedHandler 가 호출되어 예외처리

우리 프로젝트에서는 GlobalExceptionHandler 같은 공통 예외 구조를 사용하고 싶었다.

그런데 Security 예외는 Controller에 도달하기 전에 발생하기 때문에 일반적인 @RestControllerAdvice만으로는 바로 잡히지 않았다 그래서 HandlerExceptionResolver 를 사용해서 Security 예외를 GlobalExceptionHandler 위임하는 방법을 찾았다

HandlerExceptionResovler

HandlerExceptionResolver는 Spring Security의 영역이 아닌 Spring MVC 영역에 속해있는 컴포넌트이다

Spring MVC에서 HandlerExceptionResolver는 DispatcherServlet의 HandlerExceptionResolver 체인(예외 처리 체인)에 등록되어 있다 이 체인은 컨트롤러에서 발생한 예외를 처리하는 역할을 한다

따라서 각 커스텀 구현한 JwTAuthenticationEntryPoint와 JwTAccessDeniedHandler에서 HandlerExceptionResolver를 호출하여 컨트롤러에서 예외를 처리할 수 있도록 한다
그럼 우리가만든 GlobalExceptionHandler에서 예외처리를 진행할수있게된다

resolver.resolveException(
        request,
        response,
        null,
        new UnauthorizedException("인증이 필요합니다.")
);

여기서 세 번째 인자인 null은 특정 Controller 메서드에서 발생한 예외가 아니라, Security Filter 단계에서 발생한 예외이기 때문에 handler 정보가 없다는 의미다.

중요한 것은 handler가 아니라 네 번째 인자인 예외 객체다.
Spring MVC 예외 처리 체인은 이 예외 타입을 기준으로 @ExceptionHandler를 찾아 실행한다.

직접 구현한 AuthenticationEntryPoint와 AccessDeniedHandler를 SecurityConfig에 등록한다

.exceptionHandling(exception -> exception
        .authenticationEntryPoint(jwtAuthenticationEntryPoint)
        .accessDeniedHandler(jwtAccessDeniedHandler)
)
resolver.resolveException(
        request,
        response,
        null,
        new UnauthorizedException("인증이 필요합니다.")
);

여기서 세 번째 인자인 null은 특정 Controller 메서드에서 발생한 예외가 아니라, Security Filter 단계에서 발생한 예외이기 때문에 handler 정보가 없다는 의미다.

중요한 것은 handler가 아니라 네 번째 인자인 예외 객체다.
Spring MVC 예외 처리 체인은 이 예외 타입을 기준으로 @ExceptionHandler를 찾아 실행한다.

그리고 만든 커스텀 파일에서 @RequiredArgsConstructor로 생성자를 자동으로 생성하여 resolver 참조변수를 받는과정중에

@RequiredArgsConstructor
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final HandlerExceptionResolver resolver;
}
Could not autowire. There is more than one bean of 'HandlerExceptionResolver' type.

같은 에러가나왔다
HandlerExceptionResolver 타입 Bean이 하나만 있는 게 아니었기 때문이였다

해결방안

@Qualifier("handlerExceptionResolver")

여러 resolver 중에서 Spring MVC 기본 예외 처리 resolver를 사용한다고 명확하게 지정해줬다

하지만 문제는 어떤 Bean인지 지정을해줘야되는데 Lombok 자동생성자는 이런 세부설정이 완벽히 반영되지 않는다 따라서 직접 생성자를 만들어주었다

@Component
public class JwtAccessDeniedHandler
        implements AccessDeniedHandler {

    //403 예외처리
    private final HandlerExceptionResolver resolver; // HandlerExceptionResolver에게 예외 위임

    public JwtAccessDeniedHandler(
            @Qualifier("handlerExceptionResolver")
            HandlerExceptionResolver resolver
    ) {
        this.resolver = resolver;
    }

    @Override
    public void handle(
            HttpServletRequest request,
            HttpServletResponse response,
            AccessDeniedException accessDeniedException
    ) {

        resolver.resolveException(
                request,
                response,
                null,
                new AdminPermissionException("접근 권한이 없습니다.")
        );
    }
}

handler를 null로 반환하면 다음 ExceptionResolver를 찾아서 실행한다 만약 처리할 수 없는 ExceptionResolver가 없으면 예외 처리가 안되고 기존에 발생한 예외를 서블릿 밖으로 던진다

우리는 null을 반환하여 @RestControllerAdvice와 @ExceptionHandler를 사용하는 클래스에서 어떤 예외 객체를 어떻게 처리할지 확인하고 예외를 처리하도록 한다

이제 우리는 GlobalExceptionHandler에서 에러를 처리할수있다

0개의 댓글