만약 상품 웹 서비스가 있다고 가정해보자.
로그인하지 않은 사용자가 직접 URL을 입력해 상품 등록 페이지에 접근할 수 있다면 여러 문제가 발생할 수 있다.
과거에는 이러한 문제를 해결하기 위해 모든 로직에 if(session == null) 조건을 걸고 /login 으로 리다이렉트하는 방식으로 처리하였다.
그러나 이 방식은 중복 코드가 발생하고, 유지보수성에도 한계가 있었다.
오늘은 이러한 문제를 해결하기 위해 스프링이 제공하는 기능인 서블릿 필터에 대해 알아볼 것이다.
1. 서블릿 필터란 무엇인가
서블릿 필터는 웹 애플리케이션의 수문장 역할을 한다.
모든 요청은 서블릿에 도달하기 전에 필터를 먼저 거치게 된다.
흐름은 다음과 같다.
HTTP 요청 → WAS → 필터 → 서블릿(DispatcherServlet) → 컨트롤러
필터의 특징을 정리하면 다음과 같다.
- 모든 요청을 사전에 가로챌 수 있다.
- 로그인 여부와 같은 인증 검증에 적합하다.
- 부적절한 요청이라 판단되면 필터 단계에서 요청을 종료할 수 있다.
또한 필터는 체인 구조를 가진다.
HTTP 요청 → WAS → 필터1 → 필터2 → 필터3 → 서블릿 → 컨트롤러
예를 들어,
- 필터1에서는 로그를 남기고,
- 필터2에서는 로그인 검증을 하고,
- 필터3에서는 보안 관련 처리를 수행할 수 있다.
2. 필터 기본 구조
필터를 구현하기 위해서는 Filter 인터페이스를 사용한다.
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {}
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
default void destroy() {}
}
- init() : 필터가 생성될 때 한 번 호출된다.
- doFilter() : 요청이 들어올 때마다 실행되며, 실제 필터 로직이 위치한다.
- destroy() : 애플리케이션이 종료될 때 호출된다.
3. 로그 필터 예제
먼저, 모든 요청을 로그로 남기는 단순한 필터를 구현해보자.
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
log.info("log filter init");
}
@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);
} finally {
log.info("RESPONSE [{}][{}]", uuid, requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
여기서 chain.doFilter() 가 핵심이다.
이 메서드를 호출해야 다음 필터나 서블릿으로 요청이 전달된다.
만약 호출하지 않으면 요청은 해당 필터에서 끝나게 된다.
실행 예시
만약 사용자가 브라우저에서 /members/new URL을 요청했다고 해보자.
그러면 로그는 다음과 같이 찍힌다.
REQUEST [d5e3a2c1-98d4-4b32-a2f1-3c8e53b41234][/members/new]
RESPONSE [d5e3a2c1-98d4-4b32-a2f1-3c8e53b41234][/members/new]
여기서 d5e3a2c1... 부분은 요청마다 생성되는 UUID 이다.
이 값을 통해 요청과 응답이 서로 연결된 로그임을 쉽게 확인할 수 있다.
흐름 설명
- 사용자가 /members/new 요청을 보낸다.
- WAS(Web Application Server)가 요청을 받고, 먼저 LogFilter를 실행한다.
- LogFilter에서 UUID를 생성하고, 요청 URI와 함께 “REQUEST” 로그를 남긴다.
- chain.doFilter(request, response) 가 실행되면서 다음 필터(없으면 서블릿)으로 요청이 넘어간다.
- 서블릿과 컨트롤러에서 요청을 처리한 후 응답을 만든다.
- 응답이 다시 필터로 돌아오면서 “RESPONSE” 로그가 남는다.
즉, 요청이 들어올 때 한 번, 응답이 나갈 때 한 번 로그가 찍히는 구조이다.
이를 이제 로그인 체크 필터에 적용해보자.
4. 로그인 체크 필터 적용
로그인 검증도 같은 방식으로 필터에서 처리할 수 있다.
화이트리스트 경로는 통과시키고, 나머지 요청에 대해서는 세션을 확인한 뒤 로그인하지 않은 경우 로그인 페이지로 리다이렉트한다.
로그인하지 않은 사용자가 보호된 URL(예: /items/add, /orders)에 직접 접근하면 안 된다.
이제 “필터 단계”에서 한 번에 걸러내도록 만들어보자.
필터 코드
@Slf4j
public class LoginCheckFilter implements Filter {
private static final String[] whitelist = {"/", "/login", "/members/add", "/css/*"};
@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 {
log.info("인증 체크 필터 실행: {}", requestURI);
if (isLoginCheckPath(requestURI)) {
HttpSession session = httpRequest.getSession(false);
if (session == null || session.getAttribute("loginMember") == null) {
log.info("미인증 사용자 요청 {}", requestURI);
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
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<Filter> logFilter() {
FilterRegistrationBean<Filter> reg = new FilterRegistrationBean<>();
reg.setFilter(new LogFilter());
reg.setOrder(1); // 1순위
reg.setUrlPatterns(List.of("/*")); // 모든 요청
return reg;
}
@Bean
public FilterRegistrationBean<Filter> loginCheckFilter() {
FilterRegistrationBean<Filter> reg = new FilterRegistrationBean<>();
reg.setFilter(new LoginCheckFilter());
reg.setOrder(2); // 2순위 (로그 이후 인증)
reg.setUrlPatterns(List.of("/*")); // 모든 요청
return reg;
}
}
로그인 이후 redirect 처리
로그인 체크 필터는 로그인하지 않은 사용자를 /login?redirectURL=... 로 보낸다.
즉, 로그인에 성공하면 사용자가 원래 가려던 페이지로 다시 돌려보내는 흐름을 만들 수 있다.
컨트롤러에서 이렇게 처리한다.
@PostMapping("/login")
public String login(
@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
// 로그인 성공 → 세션 생성 및 저장
HttpSession session = request.getSession();
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
// 사용자가 원래 요청했던 페이지로 이동
return "redirect:" + redirectURL;
}
동작 흐름 예시
- 사용자가 로그인하지 않은 상태에서 /items/add 에 접근한다.
- 로그인 체크 필터가 동작해 세션을 확인한다. 세션이 없으므로 /login?redirectURL=/items/add 로 리다이렉트한다.
- 사용자가 로그인 폼에서 아이디와 비밀번호를 입력한다.
- 로그인 성공 시 세션에 회원 정보를 저장하고, redirectURL 파라미터(/items/add)로 다시 리다이렉트한다.
- 사용자는 로그인 직후 원래 요청했던 페이지(/items/add)로 자연스럽게 이동한다.
즉, 로그인하지 않은 유저는 로그인 페이지로 보내지고, 로그인에 성공한 유저는 원래 가려던 페이지로 바로 갈 수 있게 된다.
마무리하며
오늘은 서블릿 필터에 대해 알아보았다.
먼저, 로그인하지 않은 사용자가 직접 URL로 중요한 페이지에 접근할 수 있다는 문제 상황을 살펴보았고, 이를 해결하기 위해 서블릿 필터가 요청의 앞단에서 동작하는 구조를 확인했다.
로그 필터를 통해 요청과 응답을 기록하는 방법을 알아보았고, 로그인 체크 필터를 적용하여 세션을 기반으로 인증 여부를 검증하는 방식도 직접 코드로 작성해보았다.
정리하면, 서블릿 필터를 사용하면 다음과 같은 장점이 있었다.
- 중복되는 if(session==null) 코드를 제거할 수 있다.
- 모든 요청을 한 곳에서 제어할 수 있어 유지보수성이 높아진다.
- 로그, 인증, 보안 등 공통 관심사를 처리하기에 매우 적합하다.
다음 시간에는 스프링이 제공하는 또 다른 기능인 인터셉터(HandlerInterceptor) 를 살펴볼 것이다.
서블릿 필터와 비교했을 때 어떤 차이가 있고, 실제로 언제 인터셉터를 활용하는 것이 좋은지도 함께 정리해보겠다.
감사합니다.
'스프링' 카테고리의 다른 글
| [스프링] 예외 처리와 오류 페이지 (0) | 2025.10.01 |
|---|---|
| [스프링] 로그인 처리 3 - 스프링 인터셉터(Interceptor) (0) | 2025.09.29 |
| [스프링] 로그인 처리 1- 쿠키, 세션 (0) | 2025.09.28 |
| [스프링] Validation에 대하여 (0) | 2025.09.27 |
| [스프링] 메시지, 국제화 (0) | 2025.09.19 |
