본 프로젝트는 Spring Boot와 MySQL을 활용한 기록 프로젝트 입니다.
해당 글은 기록 프로젝트를 진행하면서 사용자 로그가 순차적으로 기록되지 않아 겪었던 사용자 식별에 대한 어려움을 해결하고자 선택한 기술적 접근 방식에 대해 설명합니다.
여러 사용자가 동시에 애플리케이션에 접근하면 각기 다른 쓰레드를 사용하기 때문에 동일한 요청에 대한 로그가 순차적으로 쌓이는 것이 아닌 순서없이 쌓이게 되는 문제를 MDC 적용과 ArgumentResolver 사용자 식별 로깅을 통해 해결하였습니다.
멀티 쓰레드 환경에서는 요청이 동시에 처리되는데, 여러 사용자가 동시에 애플리케이션에 접근하면 각기 다른 쓰레드를 사용하기 때문에 동일한 요청에 대한 로그가 순차적으로 쌓이는 것이 아닌 순서없이 쌓이게 됩니다.
따라서 사용자별 로그가 순차적으로 기록되지 않는 문제와 어떠한 사용자가 요청을 한것인지 식별하기 어려운 문제로 인해 요청별 로그 추적에 어려움을 겪었습니다.
아래와 같이 테스트를 수행하였을 때에도
문제 상황을 명확히 인지할 수 있습니다.

MDC 적용으로 해결 시도Argument Resolver에서 사용자 식별 로깅 추가로 해결 시도LoggingFilter.java
@Slf4j
@Component
public class LoggingFilter extends OncePerRequestFilter {
// 로그 메시지에 사용할 고정 키, MDC(Mapped Diagnostic Context)에 저장됩니다.
private static final String IDENTIFIER = "request_id";
// 필터에서 제외할 URL 패턴들을 정의한 화이트리스트입니다.
private static final List<String> WHITE_LIST = List.of(
"/h2-console/**",
"/favicon/**",
"/swagger-ui/**",
"/v3/api-docs/**",
"/metrics",
"/actuator/**");
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 요청 처리 시간 측정을 위해 StopWatch를 시작합니다.
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 각 요청에 대해 고유한 식별자(UUID)를 생성하여 MDC에 저장합니다.
MDC.put(IDENTIFIER, UUID.randomUUID().toString());
// 요청 헤더에서 Authorization 토큰을 가져옵니다.
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
try {
// 다음 필터 혹은 최종 리소스로 요청을 전달합니다.
filterChain.doFilter(request, response);
} finally {
// 요청 처리가 끝난 후 StopWatch를 중지합니다.
stopWatch.stop();
// 요청 로그를 기록합니다.
// 로그에 응답 상태 코드, HTTP 메서드, 요청 URI, 토큰 존재 여부, 처리 시간(ms)을 포함합니다.
log.info(LogForm.REQUEST_LOGGING_FORM,
response.getStatus(),
request.getMethod(),
request.getRequestURI(),
tokenExists(token),
stopWatch.getTotalTimeMillis());
// MDC의 내용을 초기화하여, 다른 요청에 영향을 주지 않도록 합니다.
MDC.clear();
}
}
...
}
xxx-appender.xml
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-3relative] [%thread] [request_id=%X{request_id:-startup}] %-5level - %msg%n</pattern>
</encoder>
예시 로그

이와 같이 MDC를 적용하면서 사용자별 로그를 구분해 각 사용자의 행동을 추적할 수 있으며,
다중 사용자가 동시에 요청을 보낼 때 각 사용자의 컨텍스트가 분리되어 로그가 혼합되지 않는다는 장점을 경험할 수 있었습니다.
예시 로그

MDC와 더불어 Argument Resolver에서 사용자를 식별하고 로깅을 수행하게 되면,
사용자 요청이 들어올 때마다 Argument Resolver가 먼저 실행되어 각 요청이 어느 사용자인지 명확히 식별할 수 있습니다.
(이때 요청의 고유 ID가 MDC를 통해 저장되므로, 특정 사용자의 요청을 명확하게 추적할 수 있습니다.)
실제 Grafana를 통해 로깅을 모니터링 했을 때 아래와 같이 MDC와 Argument Resolver 로깅을 통한 사용자 식별이 적용된 것을 확인해 볼 수 있습니다.
이러한 로그 데이터를 분석함으로써 특정 사용자가 자주 요청하는 API 엔드포인트, 로그인 시도 실패 기록과 패턴, 특정 사용자에게서 발생하는 예외 상황, 사용자별 응답 시간 및 성능 문제 등을 확인할 수 있습니다.

동일한 쓰레드를 사용하더라도, request_id가 다르므로 서로 다른 요청

로그 상세 추적을 위한 Panel

이와 같이 동일한 request_id를 가진 요청들을 별도로 분리하여 로그를 상세하게 추적할 수 있는 대시보드를 구성하였습니다.
이를 통해 사용자의 행동을 분석하여 서비스 최적화 및 개인화된 서비스 제공에 필요한 데이터로 활용할 수도 있습니다. 또한, 보안 측면에서 의심스러운 요청을 특정 사용자와 연결하여 신속하게 탐지할 수 있습니다.