새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.

3 분 소요

📌 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

핸들러 어댑터 전에 호출 된다.

이 메서드는 핸들러 매핑을 찾은 후에 실행할 어댑터를 찾아 실행하기 전에 호출하는 메서드 입니다.

  1. 사전 제어 : 컨트롤러 실행 여부를 결정하고 불필요한 처리를 막기 위해서
  2. 공통 준비 : 컨트롤러가 실행되기 전에 필요한 공통 로직이나 데이터를 준비하기 위해

흐름 제어는 return false를 하게 되면 해당 핸들러는 호출되지 않습니다.

이때 포워드나 리다이렉트를 사용하여 다른 핸들러로 넘길 수 있습니다.

postHandle

  • 핸들러 어댑터 호출 후에 호출된다.

컨트롤러 호출이 되고 랜더링할 ModelAndView에 대한 정보를 확인할 수 있습니다.

@ResponseBody 일 경우에는 값이 null이 들어옵니다.

afterCompletion

  • 뷰가 랜더링 된 다음 호출 됩니다.
  • 예외가 발생하면 ex에 담겨온다. 항상 호출 된다.
  • 예외가 발생하는 경우에 뒷 마무리를 할 수 있습니다.

ResourceHttpRequestHandler

사실 preHandle은 관련된 핸들러를 찾지 못하는 경우에 호출이 되지 않습니다.

스프링 컨테이너는 핸들러를 찾지 못하면 정적 리소스라고 판단하고 이 핸들러를 호출하여 찾으려고 합니다.

그래서 대부분 preHandle이 호출이 됩니다.

상태 관리

인터셉터를 사용하는 경우에는 어떻게 상태를 관리하는 것이 좋을까?

  1. HttpSession
    사용자별로 상태를 유지하며, 요청 간 데이터를 공유할 수 있다.
    다만, 로그인 인증 정보를 여기에 저장하는 것은 확장할 때 고려를 해야합니다( 해당 서버만 가지고 있는 메모리 정보이기 때문 )

    단일 서버일 경우 사용자의 로그인 상태를 저장할 수 있지만 메모리 사용량을 확인해야합니다.
    분산 서버라면 원격 메모리 디비를 사용할 수 있습니다.

  2. HttpServletRequest.setAttribute()
    단일 요청내에 담을 정보를 간단하게 저장할 수 있는 주머니 같은 존재
    컨트롤러나 뷰에서도 접근이 가능하다.

  3. ThreadLocal
    쓰레드 별로 정보를 저장할 수 있는 공간으로 스택과 같다.
    다만, 웹 요청은 쓰레드 풀을 사용하므로 초기화가 필수 이다.

  4. 외부 저장소

    레디스나 데이터 베이스에서 가져올 수 있으나 성능을 확인해야합니다.

댓글남기기