Contents
see List문제 상황: 로그는 많은데 한 요청의 흐름이 보이지 않을 때
운영 중인 Spring Boot API에서 장애를 찾을 때 가장 먼저 보는 것은 로그입니다. 하지만 실제 현장에서는 로그 양이 많아도 한 사용자의 요청이 컨트롤러, 서비스, 데이터베이스, 외부 API 호출을 거쳐 어디서 느려졌는지 바로 보이지 않는 경우가 많습니다. 특히 여러 인스턴스로 수평 확장된 환경에서는 같은 시각의 로그가 섞이고, 로드밸런서 재시도나 모바일 앱의 중복 요청까지 겹치면 원인 분석 시간이 길어집니다. 이 문제를 줄이려면 모든 요청에 추적 가능한 식별자를 붙이고, 애플리케이션 로그와 관측 지표가 같은 기준으로 연결되도록 설계해야 합니다.
핵심은 복잡한 APM 도구를 먼저 도입하는 것이 아니라 기본 로그 품질을 끌어올리는 것입니다. 요청 ID, traceId, spanId, HTTP 메서드, 경로, 상태 코드, 처리 시간, 예외 유형을 일정한 형식으로 남기면 장애 대응 속도가 크게 달라집니다. 고객이 문의한 시각과 요청 ID만 있어도 관련 로그를 바로 좁힐 수 있고, 느린 API가 특정 외부 연동 때문인지, DB 조회 때문인지, 인증 필터 때문인지 단계적으로 확인할 수 있습니다.
기본 원칙
- 클라이언트가 보낸 X-Request-Id가 있으면 이어받고, 없으면 서버에서 새로 생성합니다.
- 응답 헤더에도 같은 X-Request-Id를 내려 보내 고객지원, 프론트엔드, 백엔드가 같은 값을 기준으로 대화하게 합니다.
- 로그에는 요청 ID와 traceId를 모두 남깁니다. 요청 ID는 업무 문의 추적에 유리하고, traceId는 분산 추적과 연동하기 좋습니다.
- 예외 로그는 메시지만 남기지 말고 상태 코드, 예외 클래스, 요청 경로, 처리 시간을 함께 남깁니다.
- 개인정보나 인증 토큰은 절대 로그에 남기지 않습니다. 헤더 전체 덤프는 운영 환경에서 금지하는 편이 안전합니다.
설정 예시: Actuator와 로그 상관관계 켜기
Spring Boot에서는 Actuator와 Micrometer 기반 관측 기능을 함께 쓰면 HTTP 서버 요청, 처리 시간, 예외 정보를 표준 지표로 수집하기 쉽습니다. 아래 설정은 상태 확인 엔드포인트를 제한적으로 열고, 로그 패턴에 traceId와 spanId를 포함하는 예시입니다. 샘플링 비율은 운영 트래픽 규모에 맞게 조정해야 합니다. 트래픽이 적으면 1.0으로 시작해도 되지만, 호출량이 많다면 0.05나 0.1처럼 낮게 잡고 중요한 경로만 별도로 보강하는 방식이 현실적입니다.
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
probes:
enabled: true
tracing:
sampling:
probability: 0.1
logging:
pattern:
correlation: '[traceId=%X{traceId:-} spanId=%X{spanId:-} requestId=%X{requestId:-}] '
level:
org.springframework.web: INFO
com.example.api: INFO이 설정만으로 모든 문제가 해결되지는 않습니다. traceId는 관측 시스템과 연결되는 기준이고, requestId는 운영자가 직접 검색하기 쉬운 기준입니다. 둘 중 하나만 쓰기보다 함께 남기는 편이 좋습니다. 예를 들어 고객센터가 사용자에게 요청 ID를 안내받고, 개발자는 그 요청 ID로 로그를 찾은 뒤 같은 로그에 찍힌 traceId로 세부 구간을 확인할 수 있습니다.
요청 ID를 MDC에 넣는 필터
아래 예시는 모든 요청에 X-Request-Id를 부여하고 MDC에 requestId를 넣는 필터입니다. MDC에 들어간 값은 같은 스레드에서 출력되는 로그 패턴에 자동으로 포함됩니다. 비동기 작업이나 별도 스레드 풀로 넘어가는 작업이 많다면 TaskDecorator를 추가해 MDC 전파까지 설계해야 하지만, 일반적인 MVC 요청 흐름에서는 이 필터만으로도 장애 추적성이 크게 좋아집니다.
@Component
public class RequestIdFilter extends OncePerRequestFilter {
private static final String HEADER = "X-Request-Id";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String requestId = request.getHeader(HEADER);
if (requestId == null || requestId.isBlank()) {
requestId = UUID.randomUUID().toString();
}
MDC.put("requestId", requestId);
response.setHeader(HEADER, requestId);
long started = System.currentTimeMillis();
try {
filterChain.doFilter(request, response);
} finally {
long elapsedMs = System.currentTimeMillis() - started;
log.info("api_request method={} path={} status={} elapsedMs={}",
request.getMethod(), request.getRequestURI(), response.getStatus(), elapsedMs);
MDC.remove("requestId");
}
}
}운영에서 바로 적용하는 점검 순서
- 1단계: Nginx, 로드밸런서, 프론트엔드에서 X-Request-Id를 만들거나 그대로 전달하는지 확인합니다.
- 2단계: Spring Boot 응답 헤더에 X-Request-Id가 내려오는지 curl이나 브라우저 개발자 도구로 확인합니다.
- 3단계: 애플리케이션 로그 한 줄에 requestId, traceId, path, status, elapsedMs가 함께 찍히는지 확인합니다.
- 4단계: 4xx와 5xx를 구분해 알림 기준을 나눕니다. 4xx는 사용법이나 인증 문제일 수 있고, 5xx는 서버 장애 가능성이 높습니다.
- 5단계: 느린 요청 기준을 정합니다. 예를 들어 일반 조회 API는 800ms 이상, 외부 연동 API는 3000ms 이상처럼 업무 특성에 맞게 기준을 둡니다.
자주 하는 실수
첫 번째 실수는 디버그 로그를 많이 켜면 추적이 쉬워질 것이라고 생각하는 것입니다. 운영 환경에서 무분별한 DEBUG 로그는 저장 비용과 개인정보 노출 위험을 키우고, 정작 필요한 요청 흐름을 더 찾기 어렵게 만듭니다. 두 번째 실수는 예외가 발생한 지점에만 로그를 남기는 것입니다. 장애 분석에는 성공 요청의 기준 시간도 필요합니다. 정상 요청이 보통 몇 ms인지 알아야 특정 시점의 지연이 비정상인지 판단할 수 있습니다. 세 번째 실수는 요청 본문 전체를 남기는 것입니다. 결제, 회원, 상담, 관리자 기능에는 민감한 값이 들어갈 수 있으므로 본문 로그는 기본적으로 남기지 않고, 필요하면 필드 단위로 마스킹한 뒤 제한적으로 남겨야 합니다.
마무리 체크리스트
- 모든 API 응답에 X-Request-Id가 포함된다.
- 로그 검색만으로 특정 요청의 시작, 종료, 상태 코드, 처리 시간을 확인할 수 있다.
- traceId와 requestId가 같은 로그 라인에 남아 관측 도구와 고객지원 흐름을 연결할 수 있다.
- 토큰, 비밀번호, 주민번호, 결제 정보 같은 민감 데이터는 로그에 남지 않는다.
- 느린 요청과 5xx 오류에 대한 알림 기준이 업무별로 분리되어 있다.
Spring Boot API의 관측성은 거창한 도구보다 일관된 식별자와 좋은 로그 형식에서 시작됩니다. 요청 ID와 traceId를 함께 설계해 두면 장애가 발생했을 때 추측으로 서버를 뒤지는 시간을 줄이고, 실제 원인에 더 빨리 접근할 수 있습니다.