새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.
TIL) 인터셉터와 필터 다시 정리
📌 2025-03-31 TIL
오늘의 학습 주제
- 인터셉터와 필터 AOP 정리
- 인터셉터에서 상태 관리하기
정리
필터(filter)
package til.til.filter;
import java.io.IOException;
import java.util.UUID;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
String requestURI = httpRequest.getRequestURI();
log.info("Request URI: {}", requestURI);
String uuid = UUID.randomUUID().toString();
ResponseWrapper responseWrapper = new ResponseWrapper(httpResponse);
Exception exceptionOccurred = null;
try {
log.info("Request [{}][{}]", uuid, requestURI);
filterChain.doFilter(servletRequest, responseWrapper);
} catch (Exception e) {
exceptionOccurred = e;
throw e;
} finally {
// 응답 본문 캡처 및 로그 출력
int status = httpResponse.getStatus();
String responseBody = "";
try {
responseBody = responseWrapper.getCaptureAsString();
} catch (IOException ioe) {
log.error("Failed to capture response body", ioe);
}
log.info("Response [{}][{}] - Status: {}", uuid, requestURI, status);
log.info("Response Body [{}]: {}", uuid, responseBody);
// 에러 상황이면 클라이언트에 별도로 복사하지 않도록 함.
if (exceptionOccurred == null && !httpResponse.isCommitted()) {
// 캡처된 데이터를 원본 응답에 복사
byte[] copy = responseWrapper.getCaptureAsBytes();
// 원본 응답의 출력 스트림에 직접 기록 (이미 헤더나 Content-Length가 설정되었을 수 있음)
ServletOutputStream out = httpResponse.getOutputStream();
out.write(copy);
out.flush();
}
}
}
@Override
public void destroy() {
log.info("LogFilter destroy");
Filter.super.destroy();
}
}
특징
- 필터 체인
- 서블릿 호출전에 실행
- 프로세스 실행 전/후 로직이 하나에 있음
필터 정리
필터는 서블릿의 공통 관심사를 분리하기 위한 기술입니다.
공통관심사는 웹과 관련된 기술이 서블릿 내부에서 공통 로직에 포함된다면 유지보수나 확장성을 고려할 때 필터로 분리하는 것이 목적입니다.
서블릿 컨테이너의 모든 HTTP 요청과 응답을 수정할 수 있다는 것이 특징입니다.
프로세스 전/후 (@Around
)가 메서드 하나에서 실행하기 때문에 상태 관리가 편합니다.
스프링 컨테이너에 의존하지 않고 요청/응답을 가로챌 수 있습니다.
사용 예시
인코딩 변환, 보안 헤더나, 로그인 인증, 스프링 컨테이너와 관련없는 상태 확인등
WAS > 필터 > 서블릿(디스패처 서블릿) > …
차별점
웹 요청으로 오는 모든 파일이나 정적 데이터에 대해서도 적용이 가능합니다.
인터셉터
package til.til.interceptor;
import java.util.UUID;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String uuid = UUID.randomUUID().toString();
request.setAttribute(LOG_ID, uuid);
//@RequestMapping: HandlerMethod
//정적 리소스: ResourceHttpRequestHandler
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든정보가 포함되어 있다.
}
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
return true; //false 진행X
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse
response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse
response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
String logId = (String)request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}]", logId, requestURI);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
특징
- HandlerMapping
- Spring Context
- Exception Handling
preHandle
핸들러 어댑터 전에 호출 된다.
이 메서드는 핸들러 매핑을 찾은 후에 실행할 어댑터를 찾아 실행하기 전에 호출하는 메서드 입니다.
- 사전 제어 : 컨트롤러 실행 여부를 결정하고 불필요한 처리를 막기 위해서
- 공통 준비 : 컨트롤러가 실행되기 전에 필요한 공통 로직이나 데이터를 준비하기 위해
흐름 제어는 return false를 하게 되면 해당 핸들러는 호출되지 않습니다.
이때 포워드나 리다이렉트를 사용하여 다른 핸들러로 넘길 수 있습니다.
postHandle
- 핸들러 어댑터 호출 후에 호출된다.
컨트롤러 호출이 되고 랜더링할 ModelAndView에 대한 정보를 확인할 수 있습니다.
@ResponseBody
일 경우에는 값이 null이 들어옵니다.
afterCompletion
- 뷰가 랜더링 된 다음 호출 됩니다.
- 예외가 발생하면 ex에 담겨온다. 항상 호출 된다.
- 예외가 발생하는 경우에 뒷 마무리를 할 수 있습니다.
ResourceHttpRequestHandler
사실 preHandle
은 관련된 핸들러를 찾지 못하는 경우에 호출이 되지 않습니다.
스프링 컨테이너는 핸들러를 찾지 못하면 정적 리소스라고 판단하고 이 핸들러를 호출하여 찾으려고 합니다.
그래서 대부분 preHandle
이 호출이 됩니다.
상태 관리
인터셉터를 사용하는 경우에는 어떻게 상태를 관리하는 것이 좋을까?
-
HttpSession
사용자별로 상태를 유지하며, 요청 간 데이터를 공유할 수 있다.
다만, 로그인 인증 정보를 여기에 저장하는 것은 확장할 때 고려를 해야합니다( 해당 서버만 가지고 있는 메모리 정보이기 때문 )단일 서버일 경우 사용자의 로그인 상태를 저장할 수 있지만 메모리 사용량을 확인해야합니다.
분산 서버라면 원격 메모리 디비를 사용할 수 있습니다. -
HttpServletRequest.setAttribute()
단일 요청내에 담을 정보를 간단하게 저장할 수 있는 주머니 같은 존재
컨트롤러나 뷰에서도 접근이 가능하다. -
ThreadLocal
쓰레드 별로 정보를 저장할 수 있는 공간으로 스택과 같다.
다만, 웹 요청은 쓰레드 풀을 사용하므로 초기화가 필수 이다. -
외부 저장소
레디스나 데이터 베이스에서 가져올 수 있으나 성능을 확인해야합니다.
댓글남기기