일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 자바 ORM 표준 JPA 프로그래밍
- 밋업프로젝트
- RESTClient
- java
- 도메인 주도 개발 시작하기
- Spring
- jdbc
- 이펙티브자바
- rabbitmq-delayed-message-exchange
- kusitms
- 객체지향 쿼리 언어
- springboot
- JPQL
- scheduling messages with rabbitmq
- cicd
- 교육기획팀
- Domain Driven Design
- 최범균
- 30기
- 교육기획팀원
- 한국대학생it경영학회
- JPA
- ddd
- Spring Batch
- delayed message plugin
- 자동처리
- 영속성
- 큐시즘
- GitHub Actions
- reactive operaton
- 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' 카테고리의 다른 글
[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를 쓰는 이유 (바인딩) (0) | 2024.04.08 |