4.5 실전 고수 팁 — 대규모 트래픽 톰캣 설정과 필터/인터셉터 로깅 튜닝
단순히 엔드포인트가 작동하는 토이 프로젝트가 아닌, 초당 수천 건의 요청(TPS)이 들어오는 실무 대규모 트래픽 환경을 다루기 위한 프로덕션 레벨의 스프링 부트 튜닝 팁을 소개합니다.
🚦 1. Tomcat 스레드 풀(Thread Pool) 최적화 튜닝
스프링 부트의 기본 내장 서블릿 컨테이너인 톰캣(Tomcat) 은 브라우저 클라이언트의 1개 요청당 백엔드의 1개 스레드가 할당되어 구동하는 동기식 스레드(Thread per Request) 모델을 따릅니다. 무거운 트래픽이 쏟아져 들어오면 순식간에 스레드 풀 고갈(Thread Exhaustion)로 이어져 서버가 마비되는 병목 현상에 빠질 수 있습니다.
실제 서버 하드웨어 스펙과 요구사항에 맞게 application.yml의 톰캣 옵션을 조절하는 것이 실무의 핵심 기술입니다.
application.yml (Spring Boot 3.x 기준)
server:
tomcat:
threads:
max: 200 # 서버가 허용하는 최대 동시 연산 작업 스레드 수 (기본 200)
min-spare: 10 # 쉴 때도 대기 상태로 항시 띄워두는 최소 스레드 여유분
max-connections: 8192 # 톰캣이 열어서 쥐고 있을 수 있는 최대 소켓 커넥션 수
accept-count: 100 # 200명 꽉 찼을 때, OS 단 큐(Queue)에서 줄서기 대기표를 뽑고 버티는 허용 수
connection-timeout: 20000 # 20초 안에 패킷 안 오면 커넥션 차단 타임아웃
💡 튜닝 시 매직 넘버 주의
단순히 "트래픽 1만 명 대비" 라며 max 스레드를 10,000처럼 거대하게 잡아버리면, 오히려 서버 CPU 코어들이 스레드 간 컨텍스트 스위칭(Context Switching) 연산 만 하느라 CPU 사용률만 폭주하고 전체 서버는 최악으로 느려집니다. 통상 vCPU 수, DB 인스턴스의 물리 커넥션 수(HikariCP) 등을 고려해 벤치마크 툴 (nGrinder)로 최적값을 찾습니다.
🛡️ 2. 보안은 Filter, 비즈니스 룰은 Interceptor
실무 아키텍처에서 요청을 가로채는 필터와 인터셉터를 분리하는 코드 예시입니다.
Filter (필터): 톰캣 서블릿 앞단 전역 차단막
스프링 컨텍스트(DispatcherServlet) 도착 이전에 동작하므로 전역 보안(CORS, XSS)에 제격입니다.
@Component
public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String dirtyParam = req.getParameter("content");
// 1. 스프링 컨트롤러 도착 전에 XSS 스크립트 해킹 문자열 치환
System.out.println("Filter: XSS 스캐닝 중...");
chain.doFilter(request, response); // 다음 필터나 서블릿으로 넘김
}
}
Interceptor (인터셉터): 스프링 컨트롤러 진입 직전 룰 검사
어떤 빈(Bean)의 어떤 메서드(Method Signature)가 호출될지 모두 알 수 있습니다. 세분화된 비즈니스 룰 (등급 제한 등) 검증 레이어에 적합합니다.
@Component
public class PremiumMemberInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 클라이언트가 호출하려고 찍은 특정 클래스의 @GetMapping 컨트롤러 정보를 캐낼 수 있음
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
System.out.println("Interceptor: 대상 컨트롤러 이름 = " + method.getBean().getClass().getName());
}
String memberGrade = request.getHeader("Grade");
if (!"PREMIUM".equals(memberGrade)) {
throw new AccessDeniedException("프리미엄 회원만 이 API를 쓸 수 있습니다.");
}
return true; // true면 통과, false면 차단
}
}
📜 3. 전사적 로깅 아키텍처를 위한 MDC(Mapped Diagnostic Context) 필터
동시에 수천 명이 접속할 때 로그(Log) 파일에는 수만 줄의 기록이 서로 뒤엉켜 섞입니다. 어떤 로그 집합이 누구 1명의 요청 세트인지 짝을 맞출 수가 없습니다.
MDC는 스레드-로컬(ThreadLocal)에 고유한 키(Trace ID)를 꽂아두는 로깅 유틸리티입니다.가장 선두에 있는 Filter 단계에서 난수를 부여해 출발점으로 삼는 것이 프랙티스입니다.
// 전역 로깅 추적용 서블릿 Filter
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 0순위 무조건 최상단 실행 보장
public class MdcTracingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1. 요청 당 무작위 고유한 영수증 번호 (Trace Id) 생성
String traceId = UUID.randomUUID().toString().substring(0, 8);
// 2. MDC 스레드 로컬에 ID 심기 -> 현재 스레드가 돌아다니며 찍는 모든 로그에 붙음
MDC.put("traceId", traceId);
try {
chain.doFilter(request, response);
} finally {
// 3. 응답 다 끝나고 톰캣 스레드풀 반납 시 반드시 지워야 함! (메모리 누수, 정보 꼬임 방어)
MDC.clear();
}
}
}
이제 logback-spring.xml 로깅 패턴 파일에 %X{traceId}를 맵핑해두면, DB 쿼리를 하든 레포지토리에 에러가 터지든 상관없이 한 줄기에 묶인 완벽한 발자국([d3fda812] User 1 로그인) 추적이 가능해집니다.