티스토리 뷰
웹 서비스를 개발한다고 했을 때, 기능마다 공통으로 가지고 있는 로직이 있다. 예를 들어, 상품을 등록, 수정, 삭제하는 기능들은 검증된 사용자만 사용할 수 있는 기능들이기 때문에 사용자를 인증하는 로직이 공통으로 들어가야 한다. 이렇게 애플리케이션 여러 로직에서 공통으로 관심이 있는 있는 것을 공통 관심사(cross-cutting concern) 라고 한다.
이러한 공통 관심사를 해결하는 대표적인 기술로 스프링의 AOP가 있는데, 웹 애플리케이션이라면 서블릿 필터나 스프링 인터셉터가 더 좋은 대안이 된다.
서블릿 필터(Servlet Filter)
서블릿 필터는 J2EE 표준 스펙 기술로, HTTP 요청과 응답을 필터링하거나 수정할 수 있는 메커니즘을 제공한다. 주의할 점은, 스프링 컨테이너에서 동작하는게 아니라 톰캣(Tomcat)같은 서블릿 컨테이너에서 동작하기 때문에 다음과 같은 요청 흐름을 가지게 된다.
- HTTP Request → 서블릿 컨테이너(WAS) → 필터 → 서블릿(DispatcherServlet) → 컨트롤러
서블릿 필터를 사용하기 위해서는 jakarta.servlet.Filter
인터페이스를 구현해야 한다.
Filter 인터페이스 메서드
인터페이스를 코드로 보면 다음과 같다.
package jakarta.servlet;
import java.io.IOException;
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
default void destroy() {
}
}
코드를 보면, 구현해야할 메서드가 총 3개인데, init
메서드와 destroy
메서드는 default
메서드라 필수로 구현할 필요는 없다.
init
: 필터 초기화 메서드로 서블릿 컨테이너가 생성될 때 한 번 호출한다.doFilter
: 필터의 ULR 패턴에 맞는 클라이언트 요청이 올 때 마다 호출되며, 요청이 디스패처 서블릿에 전달 되기 전에 실행된다. 필터의 실질적인 로직을 여기에 구현하면 된다.destroy
: 필터 종료 메서드로, 서블릿 컨테이너가 종료될 때 한 번 호출된다. 사용한 리소스를 정리할 때 사용된다.
가장 중요한 doFilter
파라미터를 보면, ServletRequest
, ServletResponse
, FilterChain
총 3개가 있다. ServletRequest
, ServletResponse
객체를 사용해서 HTTP와 관련된 편의 기능을 쉽게 사용할 수 있다.
가장 중요한 FilterChain
객체는 서블릿 컨테이너가 호출할 필터들을 가지고 있는데, doFilter
의 로직이 정상적으로 수행됐다면, 마지막에 chain.doFilter
메서드를 호출해야만 다음 필터로 넘어갈 수 있다. 만약 현재 필터가 마지막 필터라면, 컨트롤러를 호출하게 된다.
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
// 로그인 인증 관련 로직...
chain.doFilter(servletRequest, servletResponse); // 호출하지 않으면 다음 필터 또는 컨트롤러가 호출 X
}
}
필터 등록 방법
필터를 등록하는 방법은 여러가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean
를 사용해 등록하는 방법이 가장 좋다.
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginFilter()); // 필터 등록
filterRegistrationBean.setOrder(1); // 순서 지정
filterRegistrationBean.addUrlPatterns("/*"); // 적용할 URL 패턴
return filterRegistrationBean;
}
}
스프링 인터셉터(Spring Interceptor)
스프링 인터셉터는 필터와 다르게 스프링 MVC가 제공하는 기술이다. 때문에 스프링 컨테이너 안에 있는 DispatcherServlet
에 의해 동작된다.
- HTTP Request → 서블릿 컨테이너(WAS) → 필터 → 서블릿(DispatcherServlet) → 인터셉터 → 컨트롤러
인터셉터 역시 URL 패턴에 맞는 모든 컨트롤러가 호출되기 전에 동작하기 때문에 공통 관심사를 분리하기 좋고, 여러 개의 인터셉터를 체인 형태로 묶을 수 있으며, 순서도 지정할 수 있다. 다만, 필터보다 편리하고 정교한 기능들을 지원한다.
구현할 인터페이스는 org.springframework.web.servlet.HandlerInterceptor
다.
HandlerInterceptor 인터페이스 메서드
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
preHandle
: 컨트롤러가 호출되기 전에 호출된다.true
를 반환하면, 다음 인터셉터 혹은 컨트롤러가 정상 호출된다.postHandle
: 컨트롤러가 호출된 후 호출된다.ModelAndView
파라미터를 사용해 뷰를 렌더링하는데 필요한 데이터를 넘겨줄 수 있다.afterCompletion
: 뷰가 렌더링 된 이후에 호출된다. 예외가 발생하면Exception
파라미터에 예외 정보가 담겨 호출된다.
위와 같이 인터셉터는 필터와 다르게 컨트롤러 호출 전(preHandle
), 호
출 후(postHandle
), 요청 완료 이후(afterCompletion
)와 같이 단계적으로 잘 세분화 되어 있다.
추가로, handler
파라미터는 컨트롤러에서 사용한 핸들러 매핑 방식에 대한 정보를 담고 있는데, @RequestMapping
을 사용했다면 HandlerMethod
타입의 핸들러 정보가 담긴다.
인터셉터 등록 방법
WebMvcConfigurer
가 제공하는 addInterceptors()
를 사용해서 인터셉터를 등록할 수 있다. 이를 위해 WebConfig
클래스가 WebMvcConfigurer
를 구현하도록 한다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor()) // 인터셉터 등록
.order(1) // 순서 지정
.addPathPatterns("/**") // 적용할 URL 패턴
.excludePathPatterns("/css/**", "/*.ico", "/error"); // 제외할 URL 패턴(화이트리스트)
}
}
필터 vs 인터셉터
두 기술의 차이를 간단하게 표로 정리해 봤다.
특징 | 서블릿 필터 | 스프링 인터셉터 |
기술 스택 | 서블릿 표준 | 스프링 MVC |
적용 범위 | 서블릿 컨테이너 전역 (정적 자원 포함) | 스프링 컨트롤러에 매핑된 요청만 처리 |
적용 시점 | 요청이 서블릿으로 전달되기 전, 응답이 클라이언트로 전달되기 전 | DispatcherServlet과 컨트롤러 사이 |
설정 방법 | FilterRagistrationBean |
WebMvcConfigurer 에서 addInterceptors() 메서드 사용 |
스프링 통합 여부 | 스프링과 독립적 | 스프링 컨텍스트와 긴밀히 통합 |
대상 | 모든 HTTP 요청 및 응답 | 스프링 컨트롤러와 관련된 요청 |
정적 리소스 처리 | 가능 (CSS, JS 등 포함) | 처리하지 않음 |
전형적인 사용 사례 | 보안, 로깅, 데이터 압축 | 인증, 공통 데이터 추가, 요청 전후 처리 로직 |
스프링을 사용하는 개발자라면, 코드 작성이나 디버깅 시에도 스프링 기술 지원을 받을 수 있기 때문에, 대부분의 경우 스프링 인터셉터를 사용하는 편이 더 나을 수 있다.
다만, 보안, 로깅, CORS, 압축같은 서비스 전역으로 사용되는 로직이나, 정적 리소스에도 공통 로직을 적용하고 싶으면 필터를 사용하는 편이 낫다. 예를 들어, Spring Security는 내부적으로 필터를 사용해 인증/인가를 구현하고 있다.
추가로, AOP를 사용할 수도 있다는 생각이 들 수 있다. 하지만 웹 애플리케이션의 경우, 파라미터로 ServletRequest, ServletResponse 객체가 넘어오기 때문에 HTTP 관련 로직을 훨씬 편리하게 작성할 수 있다. 또한, 스프링 컨트롤러는 ArgumentResolver 덕분에 다양한 파라미터를 받을 수 있는데, 이를 AOP에서 처리하기에는 다소 어려움이 있다.
결론은, 전역으로 적용되는 보안, 로깅같은 경우가 아니면, 웹 애플리케이션의 공통 관심사는 스프링 인터셉터를 사용하는 게 속 편할 수 있다.
References
'Spring&Spring Boot' 카테고리의 다른 글
[Spring/Spring Boot] Validation을 위한 @Validated 사용 방법 및 원리 이해하기(Bean Validation, BindingResult) (1) | 2024.12.16 |
---|---|
[Spring] 타임리프(Thymeleaf) 5가지 기본 표현식/자주 쓰는 구문 정리 (1) | 2024.12.03 |
[Spring] 코드로 분석해보는 Spring MVC 구조 이해(Front Controller 패턴, DispatchServet) (2) | 2024.11.08 |
[Spring] Servlet에서 JSP까지, 그리고 한계 (feat. MVC 패턴) (7) | 2024.10.09 |
스프링이란? 좋은 객체 지향 설계란? (SOLID, 스프링 컨테이너, IoC, DI) (3) | 2024.09.05 |
- Total
- Today
- Yesterday
- Spring Data JPA
- spring mvc
- JPA
- jsp
- Thymeleaf
- 선형 회귀
- 지옥에서 온 git
- 패킷 스위칭
- Do it! 정직하게 코딩하며 배우는 딥러닝 입문
- Spring Boot
- 쉘 코드
- 김영환
- 파이썬 for Beginner 연습문제
- git merge
- 스프링
- 생활코딩 javascript
- 운영체제 반효경
- 스프링 테스트
- Spring
- 스프링 컨테이너
- git branch
- 쉽게 배우는 운영체제
- Computer_Networking_A_Top-Down_Approach
- 프로그래머스
- 파이썬 for Beginner 솔루션
- Python Cookbook
- 스프링 mvc
- git
- 방명록 프로젝트
- Gradle
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |