| 일 | 월 | 화 | 수 | 목 | 금 | 토 | 
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 | 
| 12 | 13 | 14 | 15 | 16 | 17 | 18 | 
| 19 | 20 | 21 | 22 | 23 | 24 | 25 | 
| 26 | 27 | 28 | 29 | 30 | 31 | 
- redis
- 중간 장소 추천
- ddd
- kusitms
- 도메인 주도 개발 시작하기
- RESTClient
- 큐시즘
- GitHub Actions
- K3S
- java
- Domain Driven Design
- 모이삼
- 불변객체
- 모임 장소 추천
- 객체지향 쿼리 언어
- 쿠버네티스
- 이펙티브자바
- Spring Batch
- Container Registry
- 한국대학생it경영학회
- JPQL
- 약속 장소 추천
- JPA
- 중간 지점 추천
- cicd
- 백엔드
- springboot
- 자바 ORM 표준 JPA 프로그래밍
- 최범균
- Spring
- Today
- Total
코딩은 마라톤
[Springboot] Filter와 Interceptor 본문
학습하게 된 계기
지금 하고 있는 프로젝트가 기능 구현은 다 끝났지만, 인증과 관련해서 액세스 토큰만 사용하기 때문에 짧은 유효시간으로 잦은 로그인이 발생했다. 그래서 리프레시 토큰을 추가해서 안정성을 높이고자 했는데,,

모든 에러를 처리해줬음에도,,

500 에러만 계속 떠서 무슨 이유에서 에러가 발생하는지 알 수 없었다..
그 이유를 최근에서야 알게 됐는데 바로 Filter 때문이었다.

필터에서 내가 커스텀한 ServiceException이 발생 시 에러 메시지를 보내고, 그 외에 모든 에러는 500 서버 에러를 보낸다. 하지만 ServiceException은 거진 Service 레이어에서 발생하기 때문에 필터에서 디스패처 서블릿으로 넘어간 후 에러가 발생하여 필터에서 catch하지 못하는 상황이었다...
그래서 결국은 ControllerAdvice를 통해 해결했지만 필터에 대한 학습이 필요하다고 생각하여 학습하게 되었고 필터를 검색하면 따라 나오는게 인터셉터이기 때문에 둘 다 학습을 해보고자 한다 !
Filter (필터)
- Web Application에서 관리되는 영역이다.
- Client로부터 오는 요청/응답에 대해서 최초/최종 단계의 위치에 존재한다. 이를 통해 요청/응답의 정보를 변경하거나, Spring에 의해서 데이터가 변환되기 전의 순수한(객체 변환 전) Client의 요청/응답 값을 확인할 수 있다.
- 주로 request/response의 Logging 용도로 활용하거나, "인증"과 관련된 Logic들을 해당 Filter에서 처리한다. 
 예시로 들면, 나는 커스텀 에러처리는 ControllerAdvice에서 하게 하였고, JWT관련 인증은 Filter에서 처리하게 하였다.
- 필터에서 선/후 처리를 함으로써 Service Business Logic과 분리한다.
유일하게 ServletRequest, ServletResponse의 객체를 변환할 수 있다.


Filter 인터페이스에는 3가지 메서드가 존재한다.
1. init() : 웹 컨테이너에 의해 호출되며, 필터가 필터링 작업을 하기 전에 서블릿 컨테이너는 필터를 인스턴스화한 후에 1회 init() 메서드를 호출한다.
2. doFilter() : 각 컨테이너가 Client의 요청으로 인해 request/response 쌍이 FilterChain을 통해 전달될 때 호출된다. FilterChain에 연결된 필터로 넘어가는 행위라 볼 수 있다.
request (ServletRequest) : 객체에는 클라이언트의 요청이 포함되어 있습니다.
response (ServletResponse) : 객체에는 필터의 응답이 포함되어 있습니다.
chain (FilterChain) : 다음 필터나 리소스를 호출하기 위한 것입니다.
3. destroy() :  필터의 doFilter 메소드 내의 모든 스레드가 종료되거나 제한 시간이 경과한 후에만 호출된다.
Filter 활용
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
    // 1번
    if (request.getServletPath().contains("/api/v1/auth")) {
      filterChain.doFilter(request, response);
      return;
    }
    final String authHeader = request.getHeader("Authorization");
    final String jwt;
    final String userEmail;
    // 2번
    if (authHeader == null || !authHeader.startsWith("Bearer ")) {
      filterChain.doFilter(request, response);
      return;
    }
    jwt = authHeader.substring(7);
    userEmail = jwtService.extractUsername(jwt);
    /* user가 처음 로그인할 때 */
    if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
      UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
      if (jwtService.isTokenValid(jwt, userDetails)) {
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
            userDetails, null, userDetails.getAuthorities()
        );
        authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authToken);
      }
    }
    // 3번
    filterChain.doFilter(request, response);
  }
이 코드는 JWT 인증 관련한 코드인데 filterChain.doFilter(request, response)가 실행되는 부분의 전(처리)과 후(처리)를 보자.
1번 : request에서 path에 /api/v1/auth가 포함되면 doFilter를 실행한다. 상세 설명을 하면 /api/v1/auth는 회원가입, 로그인과 관련되기 때문에 토큰이 필요하지 않아서 현재 필터는 종료하고 chain에 걸린 다음 필터로 넘어가라는 의미이다. 전처리, 후처리 모두 없다.
2번 : authHeader가 null이거나 Bearer로 시작하지 않으면 토큰이 유효하지 않다고 볼 수 있기 때문에, 현재 필터는 종료하고 다음 필터로 넘어가라는 의미이다. doFilter가 실행되기 전에는 authHeader를 뽑아오는 전처리 과정이 이뤄지고 후처리는 이뤄지지 않는다.
3번 : 전처리 과정에서 토큰이 유효할 경우, 사용자에게 권한 부여를 하게된다. 이 전처리 과정을 통해 필터가 하는 일을 다 수행했으므로 후처리는 없다.
Interceptor (인터셉터)
- Filter와 매우 유사한 형태로 존재한다.
- 차이점은 Spring Context에 존재하기 때문에 AOP와 유사한 기능을 제공할 수 있다.
- 주로 인증 단계를 처리하거나, Logging를 하는데 사용한다.
- Filter와 마찬가지로 Service business logic과 분리시킨다.
인터셉터는 위 필터 사진에서 Controller 단에 위치하고 있다.
필터 -> 디스패처 서블릿 -> 인터셉터 순으로 볼 수 있다.
Interceptor 활용
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Auth {
}
--- 
@RestController
@RequestMapping("/api/private")
@Auth
@Slf4j
public class PrivateController {
    @GetMapping("/hello")
    public String hello(){
        log.info("private hello controller");
        return "private hello";
    }
}
---
@RestController
@RequestMapping("/api/public")
public class PublicController {
    // 모든 사용자들이 쓸 수 있는 api
    @GetMapping("/hello")
    public String hello() {
        return "public hello";
    }
}
@Auth 어노테이션을 가진 PrivateController와 그렇지 않은 PublicController가 있다. @Auth를 가진 PrivateController가 인터셉터에서 인증 단계를 거칠 수 있도록 하고자 한다.
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String url = request.getRequestURI();
        URI uri = UriComponentsBuilder.fromUriString(url)
                .query(request.getQueryString())
                .build()
                .toUri();
        log.info("request url : {}", url);
        boolean hasAnnotation = checkAnnotation(handler, Auth.class);
        log.info("has annotation : {}", hasAnnotation);
        // 나의 서버는 모두 public으로 동작을 하는데
        // 단, Auth 권한을 가진 요청에 대해서는 세션, 쿠키,
        if(hasAnnotation){
            // 권한 체크
            String query = uri.getQuery();
            if(query.equals("name=han")) {
                return true;
            }
            throw new AuthException();
        }
        return true;
    }
    private boolean checkAnnotation(Object handler, Class clazz){
        // resource javascript, html, ...
        if(handler instanceof ResourceHttpRequestHandler){
            return true;
        }
        // annotation check
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        if(handlerMethod.getMethodAnnotation(clazz) != null || handlerMethod.getBeanType().getAnnotation(clazz) != null){
            // Auth annotation이 있을 때 true 반환
            return true;
        }
        return false;
    }
}
AuthInterceptor은 HandlerInterceptor를 implement한다. HandlerInterceptor에서 preHandle() 메서드를 오버라이딩하는데 preHandle() 메서드는 필터의 doFilter와 유사하다. 하지만 반환 값이 true일 경우 인터셉터가 작동하고 그렇지 않으면 작동하지 않는다.
preHandle() 메서드 구현 내용은 다음과 같다.
1. @Auth 어노테이션을 갖고 있는지 checkAnnotation에서 검증한다.
2. PublicController는 모두에게 열어주고 @Auth를 가진 PrivateController는 QueryParameter가 name=han일 경우 true를 반환할 수 있게 한다.
즉, 인터셉터에서 true가 반환되면 핸들러(컨트롤러)를 실행할 수 있고, 그렇지 않으면 핸들러에 도달할 수 없게 된다.
추가로, 인터셉터 클래스를 만들었다고 인터셉터가 작동하는 것은 아니다.
@Configuration
@RequiredArgsConstructor // final로 생성된 애들을 생성자로 주입
public class MvcConfig implements WebMvcConfigurer {
    private final AuthInterceptor authInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor).addPathPatterns("/api/private/*");
    }
}
위와 같이 WebMvcConfigurer를 상속받아서 addInterceptors에 인터셉터를 추가해줘야 동작한다. 그리고 해당 인터셉터는 PrivateController에서 @Auth를 가졌는지, QueryParameter값이 설정 값과 같은지 비교하기 때문에 Path에 넣어주면 동작한다.
'Backend > Spring Boot' 카테고리의 다른 글
| [Spring] Discord Webhook을 활용하여 에러 대응하기 (1) | 2025.07.25 | 
|---|---|
| [Reactive Spring] 리액티브 프로그래밍과 오퍼레이션 (Flux, Mono) (0) | 2025.03.24 | 
| [SpringBoot + OpenAI(ChatGPT)] SpringBoot에서 OpenAI API를 이용해 연동하기 (4) | 2024.11.10 | 
| [SpringBoot] 서블릿과 서블릿 컨테이너 (1) | 2024.06.28 | 
| Request DTO에서 @Getter를 쓰는 이유 (바인딩) (1) | 2024.04.08 | 
 
                   
                  