[Spring Boot] - Logback 설정 없이 요청마다 ID 를 남겨 로깅해보자

청주는사과아님·2025년 7월 7일
0

side-story

목록 보기
6/7

최근 프로젝트를 진행하며 요청 ID 를 통해 로그를 추적 하고 싶은 욕망이 생겼습니다.

사실 이는 이전 프로젝트에서도 작업한 내용인데, Logback 에 익숙치 않아 코드가 매우 더러워 마음이 편치 않았었습니다.

그런데 최근 관련된 Spring Boot 문서를 읽던 중, 이를 아주 깔끔하게 해결하는 방법을 찾아 한번 공유해 보도록 하겠습니다.


📑 스프링 문서를 읽어보니

Spring Boot 는 다양한 logging framework 를 지원합니다.
기본 구현체로 Logback 을 사용하긴 하지만 Log4j, Java Util Logging 또한 지원합니다.

때문에 spring boot 는 framework 간 격차를 줄이기 위해 다음 목록의 스프링 변수를 시스템 변수 (Environment Variable) 로 제공, 통합하여 로깅을 진행합니다.

그런데 문서를 읽어보면 아래와 같은 logging.pattern.level 에 대한 추가 설명이 붙어있습니다.

이를 번역해 보면 다음과 같습니다.

💡 Tip

MDC 혹은 어느 커스텀한 내용을 로그에 붙이고 싶다면 LOG_LEVEL_PATTERN (Logback 의 경우 logging.level.pattern 도 가능) 을 활용할 수 있습니다.

예를들어 logging.pattern.level=user:%X{user} %5p 처럼 설정할 경우, 기본 로그 형식이 아래처럼 구성될 수 있습니다.

2019-08-30 12:30:04.031 user: someone INFO 22174 --- [ nio-8080-exec-0] demo. ControllerHandling authenticated request

이 때 user 는 MDC 의 어느 entry 로, MDC 값이 설정된 경우 위처럼 (someone) 표시됩니다.

즉, logging.pattern.level 을 이용하면 별도의 Logback 설정 없이 Spring Boot 가 MDC 값에 맞춰 로그를 찍는다는 것입니다.


✏️ MDC 에 값을 넣어주자

위 문서로 logging.pattern.level 딸깍 한번으로 로그가 찍힌다는 사실을 알았습니다.
따라서 우리가 진행해야 될 과정은 필요한 값을 MDC 로 넣어주는 기능만 구성하면 됩니다.

즉, API 요청이 들어왔을 때 request ID 를 MDC 에 설정, 끝났을 때 MDC 값을 clear 하는 기능이 필요합니다.

이는 다양한 형태로 구현될 수 있지만, 저같은 경우 ServletRequestListener 를 이용했습니다.

package jakarta.servlet;

import java.util.EventListener;

public interface ServletRequestListener extends EventListener {

    /**
     * The request is about to go out of scope of the web application. The default implementation is a NO-OP.
     *
     * @param sre Information about the request
     */
    default void requestDestroyed(ServletRequestEvent sre) {
    }

    /**
     * The request is about to come into scope of the web application. The default implementation is a NO-OP.
     *
     * @param sre Information about the request
     */
    default void requestInitialized(ServletRequestEvent sre) {
    }
}

ServletRequestListener 는 어플리케이션의 가장 첫번째 servlet (혹은 filter) 가 요청을 맞이할 때, 요청이 벗어날 때 어느 작업을 처리할 수 있게 해주는 interface 입니다. [1]

즉, 요청 ID 를 MDC 에 제공하기에 아주 적합합니다.
따라서 이를 활용해 아래처럼 요청 ID 를 MDC 에 넣고 빼주는 코드를 구성할 수 있습니다.

@Slf4j
@Component
public class RequestIdHandler implements ServletRequestListener {

    private static final int ID_LENGTH = 8;
    private static final String ridKeyOnMdc = "REQUEST_ID";

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        log.debug("Initializing request ID");
        MDC.put(ridKeyOnMdc, genRandomId());
        log.debug("Request ID has been initialized.");
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        log.debug("Destroying request ID");
        MDC.remove(ridKeyOnMdc);
        log.debug("Request ID has been destroyed.");
    }
    
    private static String genRandomId() {
        try {
            String uuid = UUID.randomUUID().toString()
                    .replaceAll("-", "")
                    .substring(0, ID_LENGTH);

            return String.format(
                    "%s-%s",
                    uuid.substring(0, ID_LENGTH / 2),
                    uuid.substring(ID_LENGTH / 2)
            );
        } catch (Exception e) {
            log.error("Failed to generate random ID", e);
            return "UNKNOWN_ID";
        }
    }
    
}

application.yaml 설정과 테스트

이제 필요한 구성품이 모두 갖춰져 있으므로, application.yaml 에 속성을 추가할 차례입니다.

logging:
  level:
    org.leisureup: debug	# debug 로그도 함 봐보기 위해 설정
  pattern:
    level: '[RID:%10X{REQUEST_ID}] %5p'

이전 문서에 나와있듯, MDC 에 설정되는 key 값 (REQUEST_ID) 를 위처럼 작성합니다.

이제 실제로 한번 테스트를 진행해 보겠습니다.
테스트를 위해 아래처럼 간단한 endpoint 를 구성해 줍니다.

@GetMapping("/hello")
public ResponseEntity<String> hello()  {
    log.info("hello;");
    try {
        return ResponseEntity.ok("hello");
    } finally {
        log.info("And good bye!");
    }
}

이제 어플리케이션을 실행해보면 다음과 같은 특징을 확인할 수 있습니다.


1. MDC 에 값이 설정되지 않으면 빈 값으로 로그가 출력된다.

ServletRequestListener 는 웹 요청이 들어올때만 작동됩니다.
때문에 위처럼 application boot 시 찍히는 로그는 요청 ID 가 빈 값으로 넣어져 출력되는 것을 볼 수 있습니다.


2. API 요청이 들어오면 요청 ID 가 포함되 로그가 출력된다.

이전 테스트로 구성한 /hello endpoint 에 요청하면 요청 ID 가 생성됩니다.
때문에 로그에서 볼 수 있듯 0ca2-57d3 가 MDC 에 생성, 삭제 되며 로그에 출력되는 것을 볼 수 있습니다.


3. (당연하지만) 어플리케이션의 범위를 벗어난 로그에도 요청 ID 가 포함된다.

기본적으로 MDC 는 ThreadLocal 에서 작동합니다.
때문에 MDC 에 저장된 요청 ID 는 thread 가 같다면 동일하고, 따라서 SQL debug 로그에도 요청 ID 가 포함된 것을 볼 수 있습니다.

(물론 async 가 얹혀지면 좀 다르겠지만)


이렇게 딸깍 한번으로 로그에 요청 ID 를 넣어봤습니다.

물론 더 커스텀한 로깅을 진행하려면 Logback 설정이 불가피하지만, 추가 설정 없이 반 필수적인 요청 ID 처리 를 완료할 수 있는게 참 매력적인것 같습니다.


Reference

profile
나 같은게... 취준?!

0개의 댓글