Spring Boot + Logback 구조적 JSON 로그 만들기 (Logging - 2)

2025. 11. 29. 16:01·Infra/LogBack

 

지난 글에서 Raspberry Pi + Elasticsearch + Kibana 환경을 기반으로 로그 분석 환경을 구축한 이유와 설치 과정을 다뤘다.
이번 글에서는 스프링 애플리케이션에서 발생하는 로그를 Kibana가 이해할 수 있는 JSON 형식으로 변환하고, 모든 요청 로그에 TraceId를 자동 포함시키는 방법을 정리해보려 한다.

 

 

 

로그를 “정형화”해야 하는 이유

Spring Boot의 기본 로그는 다음과 같은 단순 문자열(Log Line) 형태이다:

2025-11-21 13:11:02 INFO OrderService - 주문 생성 완료 user=100, order=50021

 

이러한 기본 방식에는 여러 문제점이 존재

1. 검색의 어려움

2. 에러 원친 추적 어려움

3. MSA 같은 다중 서버 환경에 비효율

4. TraceId 추적 부재 

 

즉 로그는 단순 “텍스트 출력”이 아니라, 분석 가능한 데이터(Data) 로 수집해야 가치가 생긴다.

조건 이유
JSON 구조 Kibana에서 필드 기반 분석/집계 가능
Log Level별 분리 INFO / WARN / ERROR 집계/대시보드 가능
TraceId 포함 한 요청의 전/후 로그 흐름 추적
자동화 개발자가 직접 TraceId를 넣는 방식이 아닌 자동화로 실수 방지

 

 

 

 

 

Logback JSON 포맷팅 적용 (INFO / WARN / ERROR를 각각 별도 JSON 파일로 저장)

logback.xml 

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <!-- 로그 경로 -->
  <property name="LOG_PATH" value="/app/logs"/>
  <property name="INFO_PATH" value="${LOG_PATH}/info" />
  <property name="WARN_PATH" value="${LOG_PATH}/warn" />
  <property name="ERROR_PATH" value="${LOG_PATH}/error" />

  <!-- =============== 콘솔 =============== -->
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- =============== INFO 전용 JSON 로그 파일 =============== -->
  <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${INFO_PATH}/application-info.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${INFO_PATH}/application-info-%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>7</maxHistory> <!--  7일간 보관 -->
    </rollingPolicy>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>INFO</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
      <providers>
        <timestamp/>
        <logLevel/>
        <threadName/>
        <loggerName/>
        <message/>
        <arguments/>
        <stackTrace/>
        <mdc>
          <includeMdcKeyName>traceId</includeMdcKeyName>
        </mdc>
      </providers>
    </encoder>
  </appender>

  <!-- =============== WARN 전용 JSON 로그 =============== -->
  <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${WARN_PATH}/application-warn.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${WARN_PATH}/application-warn-%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>7</maxHistory>
    </rollingPolicy>

    <!-- WARN만 -->
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>WARN</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>

    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
      <providers>
        <timestamp/>
        <logLevel/>
        <threadName/>
        <loggerName/>
        <message/>
        <arguments/>
        <stackTrace/>
        <mdc>
          <includeMdcKeyName>traceId</includeMdcKeyName>
        </mdc>
      </providers>
    </encoder>
  </appender>


  <!-- =============== ERROR 전용 JSON 로그 파일 =============== -->
  <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${ERROR_PATH}/application-error.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${ERROR_PATH}/application-error-%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>7</maxHistory> <!-- 7일간 보관  -->
    </rollingPolicy>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>ERROR</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
      <providers>
        <timestamp/>
        <logLevel/>
        <threadName/>
        <loggerName/>
        <message/>
        <arguments/>
        <stackTrace/>
        <mdc>
          <includeMdcKeyName>traceId</includeMdcKeyName>
        </mdc>
      </providers>
    </encoder>
  </appender>

  <!-- =============== 루트 로거 =============== -->
  <!--  root logger의 레벨을 설정으로 INFO 이상 레벨(INFO, WARN, ERROR) 만 출력한다 -->
  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="INFO_FILE"/>
    <appender-ref ref="WARN_FILE"/>
    <appender-ref ref="ERROR_FILE"/>
  </root>

</configuration>

 

해당 위치에 넣어준다

 

 

 

 

 

 

TraceId 자동 포함하여 요청 흐름을 한 줄로 연결하기

- Spring Boot는 멀티스레드 기반 Web 애플리케이션으로 여러 사용자가 동시에 API를 호출하면, 각 요청은 서로 다른 스레드에서 실행된다.

- 이때 같은 사용자 요청의 로그끼리 묶어서 보고 싶다면 요청마다 고유 식별값(TraceId)이 필요

- 모든 로그에 TraceId를 포함시키는 가장 안전한 방법은 Filter에서 MDC(Mapped Diagnostic Context) 사용이다.

 

MDC(Mapped Diagnostic Context)란?

- 스레드별로 독립적인 로그 컨텍스트 저장소로 보관된 값은 그 요청이 출력하는 모든 로그에 자동으로 포함된다.

 

MDC 동작 방식 순서

1 사용자가 API 요청을 보냄 (Trace-Id 포함)
2 TraceIdFilter가 실행 → MDC에 traceId 저장
3 서비스/레포지토리/컨트롤러가 log.info() 실행
4 Logback이 MDC에 저장된 traceId를 자동으로 꺼내 JSON 로그에 포함
5 요청 처리 완료 후 MDC 초기화 (누수 방지)

 

 

MDC에 값이 들어있다면 이렇게만 코드에 적더라도

log.info("결제 완료");

 

이런 형태로 로그가 찍히게 된다

log.info("[traceId = {}] 결제 완료", traceId);

 

 

 

 

그렇다면 MDC만 적용과 로그 패턴만 수정하면 traceId가 찍히는데 JSON 설정을 왜 하는가?

 

MDC 설정 후 콘솔 로그 패턴에 %X{traceId}만 포함하면 JSON 작업이 없더라도 traceId가 포함되어 출력되게 된다.

따라서 traceId 삽입 자체는 logback-JSON 없이도 가능하기 때문에 JSON 설정을 왜 추가로 하는가 궁금해졌다

<pattern>%d{HH:mm:ss} %-5level [%X{traceId}] %logger - %msg%n</pattern>

 

하지만 ELK 기반 로그 분석을 한다면 JSON이 필수인 이유

- 로그는 Kibana 입장에서 하나의 긴 문자열  ->   traceId, user, level 같은 분석 필드를 자동 인식 못함

- 패턴이 변경되면 파싱 실패

- 반면 JSON은 구조가 고정된 데이터

{
  "@timestamp": "2025-11-22T10:22:12.220Z",
  "level": "INFO",
  "logger": "OrderService",
  "message": "주문 생성 완료",
  "traceId": "ABC-1234",
  "userId": 100
}

 

그 결과 Kibana에서 다음이 가능해짐:

  • traceId = "ABC-1234" 로 요청 전체 처리 흐름 조회
  • level = ERROR 로 오류 통계/알람
  • logger.keyword = "OrderService" 로 서비스별 오류율 확인
  • userId = 100 으로 특정 사용자의 API 호출 분석

즉, JSON 로그는 로그를 검색·통계·시각화 가능한 “데이터”로 만드는 과정

 

 

 

 

TraceIdFilter

@Component
public class TraceIdFilter extends OncePerRequestFilter {

    private static final String TRACE_ID_HEADER = "Trace-Id";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

        String traceId = request.getHeader(TRACE_ID_HEADER);

        if (traceId != null && !traceId.isBlank()) {
            MDC.put("traceId", traceId);
        }

        try {
            filterChain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }
}

 

 

 

 

 

 

결론

둘의 역할은 서로 다르며 둘 다 있어야 Kibana에서 traceId 기반으로 서비스 흐름 추적이 완성된다.

MDC → TraceId를 로그에 싣는 방법
JSON → 로그를 데이터화하여 분석 가능한 형태로 저장하는 방법

 

JSON 형태로 정형화하지 않으면 로그는 ‘보는 용도’에 그치고 JSON으로 변환하는 순간부터 비로소 ‘분석 가능한 데이터’가 된다.

 

'Infra > LogBack' 카테고리의 다른 글

라즈베리파이에 Loki + Grafana로 로그 수집 시스템 구축하기  (0) 2025.12.22
FileBeat를 활용해 로그 전송 (Loggin - 3)  (0) 2025.11.22
스프링 로그 분석 환경 구축 Elasticsearch + Kibana 설치 (Logging - 1)  (0) 2025.11.21
'Infra/LogBack' 카테고리의 다른 글
  • 라즈베리파이에 Loki + Grafana로 로그 수집 시스템 구축하기
  • FileBeat를 활용해 로그 전송 (Loggin - 3)
  • 스프링 로그 분석 환경 구축 Elasticsearch + Kibana 설치 (Logging - 1)
kimfishes
kimfishes
kimfishes 님의 블로그 입니다.
  • kimfishes
    kimfishes 님의 블로그
    kimfishes
  • 전체
    오늘
    어제
    • 전체 (18) N
      • Infra (5)
        • AWS (0)
        • LogBack (4)
      • Spring Boot (13) N
        • LLM (4) N
      • 일상 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    traceId
    Qdrant
    spring ai
    ollama
    Pre-Signed URL
    Discord 알림 연동
    loging
    Redis
    캐시 스탬피드
    분산 락
    로깅
    ELK
    실시간 알림 시스템
    LLM
    UUID v7
    스프링 알림 시스템
    pgvector
    Spring boot
    cache stampede
    promtail
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
kimfishes
Spring Boot + Logback 구조적 JSON 로그 만들기 (Logging - 2)
상단으로

티스토리툴바