지난 글에서 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 |