웹 개발›Spring MVC

필터·인터셉터·예외 처리

검증과 쿠키·세션 편에 이어, 여러 요청에 공통으로 적용하는 필터·인터셉터와 예외 처리를 다룬다.


10. 필터와 인터셉터

로그인 여부 확인, 공통 로그 기록처럼 여러 요청에 공통으로 적용할 처리를 담당한다. 둘은 요청 흐름에서 놓이는 위치가 다르다. 필터는 서블릿(스프링) 바깥, 인터셉터는 디스패처 서블릿 안쪽에서 동작한다.

flowchart LR
    R[HTTP 요청] --> W[WAS] --> F[필터] --> S["서블릿<br/>(DispatcherServlet)"] --> I[인터셉터] --> C[컨트롤러]

필터 (Servlet Filter)

  • 실행 순서: HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러

  • 적절하지 않은 요청은 서블릿을 호출하지 않고 걸러낼 수 있다(필터 제한).

  • 여러 필터를 체인으로 연결할 수 있다.

  • 구현 인터페이스: Filter

@Slf4j
public class LogFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();
        String uuid = UUID.randomUUID().toString();
        try {
            log.info("REQUEST [{}][{}]", uuid, requestURI);
            chain.doFilter(request, response);   // 다음 필터(없으면 서블릿) 호출
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            log.info("RESPONSE [{}][{}]", uuid, requestURI);
        }
    }
}
@Slf4j
public class LoginCheckFilter implements Filter {

    private static final String[] whitelist = {"/", "/member/join", "/member/login", "/member/logout"};

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String requestURI = httpRequest.getRequestURI();
        try {
            if (isLoginCheckPath(requestURI)) {
                HttpSession session = httpRequest.getSession();
                if (session == null || session.getAttribute("loginMember") == null) {
                    log.info("미인증 사용자 요청 {}", requestURI);
                    httpResponse.sendRedirect("/member/login");
                    return;                       // return 하지 않으면 계속 진행됨
                }
            }
            chain.doFilter(request, response);
        } catch (Exception e) {
            throw e;                              // 톰캣까지 예외 전달
        }
    }

    // 화이트리스트는 인증 체크 제외
    private boolean isLoginCheckPath(String requestURI) {
        return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
    }
}
@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new LogFilter());
        bean.setOrder(1);                         // 순서. 낮을수록 먼저
        bean.addUrlPatterns("/*");                // 적용 URL 패턴
        return bean;
    }

    @Bean
    public FilterRegistrationBean loginCheckFilter() {
        FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new LoginCheckFilter());
        bean.setOrder(2);
        bean.addUrlPatterns("/*");
        return bean;
    }
}

인터셉터 (Spring Interceptor)

  • 실행 순서: HTTP 요청 → WAS → 필터 → 서블릿 → 인터셉터 → 컨트롤러

  • 구현 인터페이스: HandlerInterceptor

  • 필터는 doFilter() 하나뿐이지만, 인터셉터는 호출 전(preHandle) / 호출 후(postHandle) / 요청 완료 후(afterCompletion) 단계별로 기능을 제공한다.

sequenceDiagram
    participant C as 클라이언트
    participant I as 인터셉터
    participant H as 컨트롤러
    participant V as 뷰
    C->>I: 요청
    I->>I: preHandle() — false면 여기서 중단
    I->>H: 컨트롤러 호출
    H-->>I: 반환
    I->>I: postHandle() — 컨트롤러 예외 시 호출 안 됨
    I->>V: 뷰 렌더링
    V-->>I: 렌더링 완료
    I->>I: afterCompletion() — 예외와 무관하게 항상 호출
    I-->>C: 응답
@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    public static final String LOG_ID = "logId";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String requestURI = request.getRequestURI();
        String uuid = UUID.randomUUID().toString();
        request.setAttribute(LOG_ID, uuid);

        // @RequestMapping → HandlerMethod / 정적 리소스 → ResourceHttpRequestHandler
        if (handler instanceof HandlerMethod hm) {
            // 컨트롤러 메소드의 모든 정보 접근 가능
        }
        log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
        return true;                              // false면 진행 중단
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler, ModelAndView modelAndView) {
        // 컨트롤러에서 예외 발생 시 호출되지 않음
        log.info("postHandle [{}]", modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) {
        String logId = (String) request.getAttribute(LOG_ID);
        log.info("RESPONSE [{}][{}]", logId, request.getRequestURI());
        if (ex != null) {
            log.error("afterCompletion error!!", ex);   // 예외와 무관하게 항상 호출
        }
    }
}
@Configuration
public class WebConfig implements WebMvcConfigurer {   // WebMvcConfigurer 구현 후 메서드 오버라이드

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
            .order(1)
            .addPathPatterns("/**")
            .excludePathPatterns("/css/**", "/*.ico", "/error");

        registry.addInterceptor(new LoginCheckInterceptor())
            .order(2)
            .addPathPatterns("/**")
            .excludePathPatterns("/", "/member/join", "/member/login", "/member/logout",
                                 "/css/**", "/*.ico", "/error");
    }
}

필터 vs 인터셉터

인터셉터가 더 정교하다. addPathPatterns/excludePathPatterns로 URL 패턴을 세밀하게 지정할 수 있고, 핸들러 정보(HandlerMethod)에도 접근할 수 있다. 스프링 환경에서는 인터셉터를 우선 고려한다.


11. 예외 처리와 오류 페이지

스프링 예외 처리 흐름

예외를 잡지 않으면 들어왔던 경로를 거꾸로 거슬러 WAS까지 전파된다.

flowchart LR
    C["컨트롤러<br/>(예외 발생)"] --> I[인터셉터] --> S[서블릿] --> F[필터] --> W[WAS]

오류 발생 처리

  • throw Exception

  • response.sendError(HTTP 상태코드, 오류 메시지)

스프링 부트 오류 페이지

스프링 부트는 BasicErrorController를 기본 제공한다. 다음 우선순위로 뷰를 선택한다.

뷰 템플릿
    resources/templates/error/500.html
    resources/templates/error/5xx.html
정적 리소스
    resources/static/error/404.html
    resources/static/error/4xx.html
적용 대상이 없으면
    resources/templates/error.html

API 예외 처리 — ExceptionResolver

우선순위

ExceptionResolver

역할

높음

ExceptionHandlerExceptionResolver

@ExceptionHandler 처리. API 예외 처리는 대부분 이걸로 해결

중간

ResponseStatusExceptionResolver

@ResponseStatus, ResponseStatusException을 HTTP 상태 코드로 변환

낮음

DefaultHandlerExceptionResolver

스프링 내부 기본 예외 처리

@ExceptionHandler 와 @ControllerAdvice

먼저 클라이언트에 내려 줄 오류 응답 DTO를 정의한다.

@Data
@AllArgsConstructor             // (code, message)를 받는 생성자 자동 생성
public class ErrorResult {
    private String code;        // 오류 분류 코드 (예: "BAD", "INTERNAL")
    private String message;     // 사용자에게 보여 줄 메시지
}

그리고 @RestControllerAdvice에서 예외 종류별로 이 DTO를 반환한다.

@RestControllerAdvice           // 모든 컨트롤러에 적용 + @ResponseBody 포함
public class ExControllerAdvice {

    // IllegalArgumentException → 400 Bad Request + JSON 본문
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResult illegalExHandler(IllegalArgumentException e) {
        return new ErrorResult("BAD", e.getMessage());
    }

    // 그 외 모든 예외 → 500 Internal Server Error (마지막 안전망)
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResult exHandler(Exception e) {
        return new ErrorResult("INTERNAL", "내부 오류가 발생했습니다.");
    }
}

IllegalArgumentException이 발생하면 클라이언트는 다음과 같은 JSON 응답을 받는다.

{
    "code": "BAD",
    "message": "회원이 없습니다. id=100"
}
  • @ControllerAdvice는 대상 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여한다. 대상을 지정하지 않으면 모든 컨트롤러에 적용된다.

  • @RestControllerAdvice는 @ControllerAdvice + @ResponseBody다.

// 대상 컨트롤러 지정 방법
@ControllerAdvice(annotations = RestController.class)
@ControllerAdvice("org.example.controllers")
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})

API 요청 검증 실패 처리

8. Bean Validation에서 본 검증은 응답 방식에 따라 실패를 다루는 법이 다르다.

  • 화면(폼) 컨트롤러: @Valid + BindingResult로 받아, 오류가 있으면 폼 화면으로 되돌린다.

  • API(@RequestBody) 컨트롤러: BindingResult 없이 @Valid만 쓰면, 검증 실패 시 MethodArgumentNotValidException 이 발생한다. 이 예외를 @RestControllerAdvice에서 잡아 JSON 오류로 변환한다.

@PostMapping("/api/members")
public Member apiSave(@Valid @RequestBody MemberForm form) {   // BindingResult 없음
    return memberService.save(form);                           // 검증 실패 시 예외가 던져짐
}
@RestControllerAdvice
public class ExControllerAdvice {

    // @RequestBody 검증 실패 → 400 + 어떤 필드가 왜 틀렸는지 응답
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResult validationHandler(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .map(err -> err.getField() + ": " + err.getDefaultMessage())
                .collect(Collectors.joining(", "));
        return new ErrorResult("VALIDATION", message);
    }
}
{
    "code": "VALIDATION",
    "message": "name: 이름은 필수입니다., age: 1~120 사이여야 합니다."
}

예외 이름이 상황마다 다르다

같은 Bean Validation이라도 트리거 방식에 따라 던져지는 예외가 다르다. @RequestBody + @Valid는 MethodArgumentNotValidException, 클래스 레벨 @Validated의 메서드 검증은 ConstraintViolationException 이다. 둘 다 @ExceptionHandler로 잡아 동일한 오류 응답으로 변환할 수 있다.

마지막 수정 2026년 10월 22일