2026년 Spring Boot 구조화 로깅: Logback과 OpenTelemetry로 운영 JSON 출력
Spring Boot 4.x 구조화 로깅 완벽 가이드입니다. 네이티브 JSON 출력, OpenTelemetry Starter, MDC 추적, 운영 옵저버빌리티를 위한 ELK Stack 연동을 다룹니다.

전통적인 텍스트 로그는 운영 환경에서 빠르게 관리 불가능한 상태가 됩니다. 수백 개의 인스턴스가 초당 수천 줄의 로그를 생성하면 특정 오류를 찾는 일은 악몽이 됩니다. JSON 형식의 구조화 로그는 모든 이벤트를 쿼리 가능하고 자동으로 분석 가능하게 만들어 이러한 상황을 완전히 바꿉니다.
Spring Boot 4.x(Spring Framework 7 기반)는 ECS, Logstash, GELF 형식의 구조화된 JSON 로깅을 네이티브로 지원합니다. 새로운 spring-boot-starter-opentelemetry는 외부 의존성 없이 통합 옵저버빌리티를 제공합니다.
Spring Boot에서 구조화 로그를 도입하는 이유
전통적인 텍스트 로그의 한계
전형적인 텍스트 로그는 다음과 같은 모습입니다.
2026-08-22 10:15:32.456 INFO [order-service,abc123] c.e.s.OrderService - Order created for user john@example.com, amount: 150.00€, items: 3이 형식은 운영 환경에서 여러 문제를 발생시킵니다. 특정 정보를 추출하려면 복잡하고 깨지기 쉬운 정규 표현식이 필요합니다. 서비스 간 상관관계 분석에는 엄격한 규약이 필요한데 팀마다 해석이 달라집니다. Elasticsearch 같은 분석 도구는 이러한 비구조화 문자열을 효율적으로 인덱싱하기 어렵습니다.
JSON 형식의 장점
같은 이벤트를 JSON으로 표현하면 즉시 활용할 수 있습니다.
{
"@timestamp": "2026-08-22T10:15:32.456Z",
"level": "INFO",
"logger": "com.example.service.OrderService",
"message": "Order created",
"service": "order-service",
"traceId": "abc123",
"userId": "john@example.com",
"orderId": "ORD-789456",
"amount": 150.00,
"currency": "EUR",
"itemCount": 3
}모든 필드를 필터링하고 집계할 수 있게 됩니다. Elasticsearch 쿼리로 최근 15분 동안 100유로를 초과하는 주문 전체를 즉시 찾을 수 있습니다. Kibana 대시보드는 수동 파싱 없이 트렌드를 시각화합니다. 이는 Spring Boot 면접 질문에서도 특히 관련성이 높으며, 운영 옵저버빌리티 패턴을 이해하는 것이 시니어 후보자를 구별하는 요소가 됩니다.
Spring Boot 4.x 네이티브 구조화 로깅 설정
구조화 JSON 로그 활성화
Spring Framework 7 기반의 Spring Boot 4.x는 logging.structured 속성을 통해 성숙한 구조화 로깅 지원을 도입했습니다. 이 방식은 추가 의존성을 전혀 필요로 하지 않으며 Logback 1.5.38과 직접 통합됩니다.
# application.yml
# Native structured logging configuration for Spring Boot 4.x
logging:
structured:
# Output format: ecs (Elastic), logstash, gelf
format:
console: ecs
file: ecs
file:
name: /var/log/app/application.log
level:
root: INFO
com.example: DEBUGECS(Elastic Common Schema) 형식은 추가 설정 없이 Elasticsearch와 Kibana와의 직접적인 호환성을 보장합니다.
JSON 필드 커스터마이징
각 로그에 비즈니스 필드를 추가하기 위해 Spring Boot는 추가 속성 설정을 허용합니다.
# application.yml
# Custom fields in structured logs
logging:
structured:
format:
console: ecs
ecs:
# Service information added to every log
service:
name: ${spring.application.name}
version: ${app.version:1.0.0}
environment: ${spring.profiles.active:default}
node-name: ${HOSTNAME:unknown}// Programmatic configuration for additional fields
package com.example.logging.config;
import org.springframework.boot.logging.structured.StructuredLogFormatterCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LoggingConfig {
@Bean
StructuredLogFormatterCustomizer<EcsStructuredLogFormatter> ecsCustomizer() {
return formatter -> formatter
// Adds static fields to all logs
.addStaticField("team", "backend")
.addStaticField("region", System.getenv("AWS_REGION"))
// Customizes exception formatting
.setIncludeStacktrace(true)
.setStacktraceMaxLength(5000);
}
}이 필드들은 모든 로그 라인에 표시되어 대시보드에서 팀이나 리전별로 필터링하기 쉽게 만듭니다.
완전한 옵저버빌리티를 위한 OpenTelemetry Starter
Spring Boot 4의 새로운 표준
Spring Boot 4.0은 spring-boot-starter-opentelemetry를 도입하여 이전에 존재했던 복잡한 다중 의존성 설정을 대체했습니다. 이 단일 의존성은 트레이스, 메트릭, 로그 상관관계를 포함한 벤더 중립적 옵저버빌리티를 제공합니다.
<!-- pom.xml -->
<!-- OpenTelemetry starter for Spring Boot 4.x -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>
<!-- Log correlation for Logback -->
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-logback-appender-1.0</artifactId>
<version>2.21.0-alpha</version>
</dependency>이 스타터에는 OpenTelemetry API, Micrometer tracing 브릿지, OTLP 익스포터가 포함되어 있습니다. Spring Cloud Sleuth는 현재 레거시로 간주되며, OpenTelemetry가 분산 추적의 업계 표준이 되었습니다.
OpenTelemetry와 구조화 로그 설정
# application.yml
# OpenTelemetry configuration with structured logging
spring:
application:
name: order-service
group: commerce
otel:
exporter:
otlp:
endpoint: http://otel-collector:4317
protocol: grpc
resource:
attributes:
service.namespace: production
deployment.environment: prod
logging:
structured:
format:
console: ecs
pattern:
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"트레이스 ID와 스팬 ID는 MDC에 자동으로 주입되어 서비스 간 분산 추적과 로그를 상관시킵니다. 운영 모니터링 설정에 대한 자세한 내용은 Micrometer와 Prometheus를 사용한 Spring Boot Actuator 가이드를 참조하십시오.
ZipkinWithOpenTelemetryTracingAutoConfiguration은 지원 중단되었으며 Spring Boot 4.2에서 제거될 예정입니다. 향후 호환성을 위해 네이티브 OTLP 익스포터로 마이그레이션하십시오.
JSON 인코더를 사용한 클래식 Logback 설정
고급 커스터마이징을 위한 Logstash Encoder
고급 커스터마이징이 필요하거나 이전 Spring Boot 버전에서 마이그레이션하는 경우, Logstash Logback Encoder 9.0을 계속 사용할 수 있습니다. 버전 9.0은 Jackson 3.0과 Java 17 이상이 필수입니다.
<!-- pom.xml -->
<!-- Dependency for JSON logging with Logback (Jackson 3 required) -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>9.0</version>
</dependency>전체 Logback 설정
logback-spring.xml 파일은 출력 형식에 대한 완전한 제어를 제공합니다.
<!-- src/main/resources/logback-spring.xml -->
<!-- Logback configuration for structured JSON logs -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Spring Boot properties -->
<springProperty scope="context" name="appName" source="spring.application.name" defaultValue="app"/>
<springProperty scope="context" name="appVersion" source="app.version" defaultValue="1.0.0"/>
<!-- JSON console appender for production -->
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<!-- Custom fields added to every log -->
<customFields>{"service":"${appName}","version":"${appVersion}"}</customFields>
<!-- Includes MDC (tracing context) -->
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>spanId</includeMdcKeyName>
<includeMdcKeyName>userId</includeMdcKeyName>
<includeMdcKeyName>requestId</includeMdcKeyName>
<!-- ISO8601 timestamp format -->
<timestampPattern>yyyy-MM-dd'T'HH:mm:ss.SSSZ</timestampPattern>
<!-- Complete stack traces -->
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
<maxDepthPerThrowable>30</maxDepthPerThrowable>
<maxLength>4096</maxLength>
<shortenedClassNameLength>36</shortenedClassNameLength>
<rootCauseFirst>true</rootCauseFirst>
</throwableConverter>
</encoder>
</appender>
<!-- Rolling JSON file appender -->
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/${appName}/application.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/${appName}/application.%d{yyyy-MM-dd}.%i.json.gz</fileNamePattern>
<maxHistory>30</maxHistory>
<maxFileSize>100MB</maxFileSize>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"service":"${appName}","version":"${appVersion}"}</customFields>
</encoder>
</appender>
<!-- Text appender for development -->
<appender name="TEXT_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %highlight(%-5level) [%thread] %cyan(%logger{36}) - %msg%n</pattern>
</encoder>
</appender>
<!-- Activation by Spring profile -->
<springProfile name="prod,staging">
<root level="INFO">
<appender-ref ref="JSON_CONSOLE"/>
<appender-ref ref="JSON_FILE"/>
</root>
</springProfile>
<springProfile name="dev,local">
<root level="DEBUG">
<appender-ref ref="TEXT_CONSOLE"/>
</root>
</springProfile>
</configuration>이 설정은 운영 환경에서만 JSON 로그를 활성화하고 개발 환경에서는 가독성 좋은 로그를 유지합니다.
분산 추적을 위한 MDC
추적 컨텍스트 전파
MDC(Mapped Diagnostic Context)는 요청이나 추적 식별자 같은 컨텍스트 정보를 모든 로그에 추가합니다.
// Filter for automatic trace context injection
package com.example.logging.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TracingFilter extends OncePerRequestFilter {
// Standard MDC keys for tracing
private static final String TRACE_ID_KEY = "traceId";
private static final String SPAN_ID_KEY = "spanId";
private static final String REQUEST_ID_KEY = "requestId";
private static final String USER_ID_KEY = "userId";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// Retrieve or generate trace identifiers
String traceId = extractOrGenerate(request, "X-Trace-Id", TRACE_ID_KEY);
String spanId = generateSpanId();
String requestId = extractOrGenerate(request, "X-Request-Id", REQUEST_ID_KEY);
String userId = request.getHeader("X-User-Id");
// Inject into MDC to appear in all logs
MDC.put(TRACE_ID_KEY, traceId);
MDC.put(SPAN_ID_KEY, spanId);
MDC.put(REQUEST_ID_KEY, requestId);
if (userId != null) {
MDC.put(USER_ID_KEY, userId);
}
// Propagate to responses for inter-service chaining
response.setHeader("X-Trace-Id", traceId);
response.setHeader("X-Request-Id", requestId);
filterChain.doFilter(request, response);
} finally {
// Clean MDC after each request
MDC.clear();
}
}
private String extractOrGenerate(HttpServletRequest request, String header, String key) {
String value = request.getHeader(header);
return value != null ? value : UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
private String generateSpanId() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 8);
}
}요청 처리 도중 발생하는 모든 로그에는 이 식별자들이 자동으로 포함됩니다.
비즈니스 코드에서 MDC 사용
// Business service with enriched contextual logging
package com.example.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public Order createOrder(CreateOrderRequest request) {
// Add business information to MDC context
MDC.put("orderId", request.getOrderId());
MDC.put("customerId", request.getCustomerId());
try {
log.info("Creating order with {} items", request.getItems().size());
// Business logic...
Order order = processOrder(request);
log.info("Order created successfully, total: {} {}",
order.getTotal(), order.getCurrency());
return order;
} catch (Exception e) {
// Exception appears with full MDC context
log.error("Failed to create order", e);
throw e;
} finally {
// Clean business keys added
MDC.remove("orderId");
MDC.remove("customerId");
}
}
}생성된 JSON 로그에는 디버깅에 필요한 모든 정보가 포함됩니다.
{
"@timestamp": "2026-08-22T10:15:32.456Z",
"level": "INFO",
"logger": "com.example.service.OrderService",
"message": "Order created successfully, total: 150.00 EUR",
"traceId": "a1b2c3d4e5f67890",
"spanId": "12345678",
"requestId": "req-abc-123",
"userId": "user-456",
"orderId": "ORD-789",
"customerId": "CUST-321"
}Spring Boot 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
성능을 위한 비동기 로깅
스레드 풀 설정
운영 환경에서 동기적인 로그 쓰기는 요청 지연 시간에 영향을 줍니다. 비동기 어펜더는 메인 스레드와 로깅을 분리합니다.
<!-- logback-spring.xml -->
<!-- High-performance asynchronous appender configuration -->
<appender name="ASYNC_JSON" class="ch.qos.logback.classic.AsyncAppender">
<!-- Pending log buffer size -->
<queueSize>1024</queueSize>
<!-- Never block the calling thread -->
<neverBlock>true</neverBlock>
<!-- Threshold before dropping DEBUG/TRACE logs -->
<discardingThreshold>20</discardingThreshold>
<!-- Include caller information (expensive) -->
<includeCallerData>false</includeCallerData>
<!-- Actual appender for writing -->
<appender-ref ref="JSON_FILE"/>
</appender>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ASYNC_JSON"/>
</root>
</springProfile>Spring Boot 4.1.1에서는 JSON 인코딩 실패가 동일 스레드의 다음 로그 이벤트를 손상시킬 수 있는 심각한 문제(#51371)가 수정되었습니다. 조용한 로그 손상을 방지하려면 4.0.x 또는 4.1.0에서 업그레이드하십시오.
로깅 시스템 메트릭
로깅 시스템 자체를 모니터링하면 조용한 로그 손실을 방지할 수 있습니다.
// Exposing Logback metrics via Micrometer
package com.example.logging.metrics;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.classic.AsyncAppender;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import java.util.Iterator;
@Component
public class LoggingMetrics {
private final MeterRegistry registry;
public LoggingMetrics(MeterRegistry registry) {
this.registry = registry;
}
@PostConstruct
void registerMetrics() {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
Logger rootLogger = context.getLogger(Logger.ROOT_LOGGER_NAME);
// Iterate through appenders to find AsyncAppenders
Iterator<Appender<ILoggingEvent>> it = rootLogger.iteratorForAppenders();
while (it.hasNext()) {
Appender<ILoggingEvent> appender = it.next();
if (appender instanceof AsyncAppender asyncAppender) {
registerAsyncMetrics(asyncAppender);
}
}
}
private void registerAsyncMetrics(AsyncAppender appender) {
String appenderName = appender.getName();
// Current queue size
Gauge.builder("logback.async.queue.size", appender, AsyncAppender::getQueueSize)
.tag("appender", appenderName)
.description("Current async appender queue size")
.register(registry);
// Remaining capacity
Gauge.builder("logback.async.queue.remaining", appender, AsyncAppender::getRemainingCapacity)
.tag("appender", appenderName)
.description("Remaining capacity in async queue")
.register(registry);
// Number of dropped logs
Gauge.builder("logback.async.discarded", appender, AsyncAppender::getNumberOfElementsInQueue)
.tag("appender", appenderName)
.description("Number of discarded log events")
.register(registry);
}
}logback.async.queue.remaining < 100에 대한 Prometheus 알람은 로그 손실 위험을 사전에 알려줍니다.
ELK Stack 연동
Filebeat 설정
Filebeat는 JSON 파일을 수집해 변환 없이 Elasticsearch로 전송합니다.
# filebeat.yml
# Filebeat configuration for Spring Boot JSON logs
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/*/application.json
# Automatic JSON parsing
json:
keys_under_root: true
overwrite_keys: true
add_error_key: true
message_key: message
processors:
# Add Kubernetes metadata if available
- add_kubernetes_metadata:
host: ${NODE_NAME}
matchers:
- logs_path:
logs_path: "/var/log/containers/"
# Parse timestamp
- timestamp:
field: "@timestamp"
layouts:
- '2006-01-02T15:04:05.000Z'
- '2006-01-02T15:04:05.000-07:00'
test:
- '2026-08-22T10:15:32.456Z'
output.elasticsearch:
hosts: ["elasticsearch:9200"]
index: "logs-%{[service]}-%{+yyyy.MM.dd}"
pipeline: "spring-boot-logs"
setup.template:
name: "logs"
pattern: "logs-*"데이터 보강을 위한 Elasticsearch 파이프라인
{
"description": "Spring Boot logs enrichment",
"processors": [
{
"geoip": {
"field": "client.ip",
"target_field": "client.geo",
"ignore_missing": true
}
},
{
"user_agent": {
"field": "user_agent.original",
"target_field": "user_agent",
"ignore_missing": true
}
},
{
"set": {
"field": "event.ingested",
"value": "{{_ingest.timestamp}}"
}
},
{
"script": {
"description": "Classify log level severity",
"source": "def level = ctx.level; if (level == 'ERROR') ctx.severity = 4; else if (level == 'WARN') ctx.severity = 3; else if (level == 'INFO') ctx.severity = 2; else ctx.severity = 1;"
}
}
]
}Spring Boot 로깅 운영 환경 모범 사례
체계적으로 포함해야 할 정보
각 로그에는 디버깅과 상관관계 분석을 위한 최소 정보가 포함되어야 합니다.
// Helper for consistent structured logs
package com.example.logging;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.Map;
import java.util.function.Supplier;
public final class StructuredLogger {
private final Logger delegate;
private StructuredLogger(Class<?> clazz) {
this.delegate = LoggerFactory.getLogger(clazz);
}
public static StructuredLogger getLogger(Class<?> clazz) {
return new StructuredLogger(clazz);
}
// Log with temporary business context
public void info(String message, Map<String, String> context) {
try {
context.forEach(MDC::put);
delegate.info(message);
} finally {
context.keySet().forEach(MDC::remove);
}
}
// Log with supplier for lazy evaluation
public void debug(Supplier<String> messageSupplier, Map<String, String> context) {
if (delegate.isDebugEnabled()) {
try {
context.forEach(MDC::put);
delegate.debug(messageSupplier.get());
} finally {
context.keySet().forEach(MDC::remove);
}
}
}
// Error log with full context
public void error(String message, Throwable t, Map<String, String> context) {
try {
context.forEach(MDC::put);
delegate.error(message, t);
} finally {
context.keySet().forEach(MDC::remove);
}
}
}// Usage in business code
private static final StructuredLogger log = StructuredLogger.getLogger(PaymentService.class);
public void processPayment(Payment payment) {
log.info("Processing payment", Map.of(
"paymentId", payment.getId(),
"amount", String.valueOf(payment.getAmount()),
"currency", payment.getCurrency(),
"method", payment.getMethod().name()
));
}제외해야 할 민감 정보
로그에는 절대로 개인 정보나 민감 데이터를 포함해서는 안 됩니다.
// Sensitive data masking utility
package com.example.logging.filter;
import java.util.regex.Pattern;
public final class SensitiveDataFilter {
// Sensitive data patterns to mask
private static final Pattern EMAIL_PATTERN =
Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");
private static final Pattern CREDIT_CARD_PATTERN =
Pattern.compile("\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b");
private static final Pattern PASSWORD_PATTERN =
Pattern.compile("(?i)(password|pwd|secret|token)[\"']?\\s*[:=]\\s*[\"']?[^\\s,}\"']+");
private SensitiveDataFilter() {}
// Utility method to mask data
public static String maskSensitiveData(String input) {
if (input == null) return null;
String result = input;
result = EMAIL_PATTERN.matcher(result).replaceAll("[EMAIL_MASKED]");
result = CREDIT_CARD_PATTERN.matcher(result).replaceAll("[CARD_MASKED]");
result = PASSWORD_PATTERN.matcher(result).replaceAll("=[REDACTED]");
return result;
}
}적절한 로그 레벨
| 레벨 | 사용 상황 | 예시 |
|---|---|---|
| ERROR | 개입이 필요한 장애 | 복구 불가능한 예외, 중요 트랜잭션 실패, 외부 서비스 불가 |
| WARN | 비정상이지만 처리된 상황 | 재시도 진행 중, 성능 저하, 리소스 한계 근접 |
| INFO | 중요한 비즈니스 이벤트 | 트랜잭션 시작/종료, 중요 상태 변경, 주요 사용자 액션 |
| DEBUG | 진단 정보 | 실행 세부 사항, 중요 변수 값, 분기 결정 |
| TRACE | 매우 상세한 정보 | 메서드 진입/종료, 객체 전체 내용, 루프와 반복 |
구조화 로그 테스트와 검증
JSON 구조에 대한 단위 테스트
// Structured log validation tests
package com.example.logging;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import static org.assertj.core.api.Assertions.assertThat;
class StructuredLoggingTest {
private ListAppender<ILoggingEvent> listAppender;
private Logger logger;
@BeforeEach
void setUp() {
logger = (Logger) LoggerFactory.getLogger(StructuredLoggingTest.class);
listAppender = new ListAppender<>();
listAppender.start();
logger.addAppender(listAppender);
}
@Test
void shouldIncludeMdcFieldsInLog() {
// Given
MDC.put("traceId", "test-trace-123");
MDC.put("userId", "user-456");
// When
logger.info("Test message with MDC context");
// Then
ILoggingEvent event = listAppender.list.get(0);
assertThat(event.getMDCPropertyMap())
.containsEntry("traceId", "test-trace-123")
.containsEntry("userId", "user-456");
MDC.clear();
}
@Test
void shouldLogExceptionWithStackTrace() {
// Given
Exception testException = new RuntimeException("Test error");
// When
logger.error("Operation failed", testException);
// Then
ILoggingEvent event = listAppender.list.get(0);
assertThat(event.getThrowableProxy()).isNotNull();
assertThat(event.getThrowableProxy().getMessage()).isEqualTo("Test error");
}
}구조화 로깅과 잘 맞는 통합 테스트 패턴에 대해서는 Testcontainers Spring Boot 통합 테스트 가이드를 참조하십시오.
소스
- Spring Boot 4.1.0 릴리스 공지 - 옵저버빌리티 업데이트를 포함한 Spring Boot 4.1.0 기능
- Spring Boot GitHub 릴리스 - JSON 인코딩 손상 버그 수정(#51371)을 포함한 버전 4.1.1
- Logstash Logback Encoder 9.0 - Jackson 3 마이그레이션 및 Java 17 요구사항
- Spring Boot와 OpenTelemetry - 공식 OpenTelemetry 통합 가이드
Spring Boot 4.x 구조화 로깅 체크리스트
- ECS, Logstash, 또는 GELF 형식의 네이티브 구조화 로깅은
logging.structured.format설정만으로 활성화 가능 spring-boot-starter-opentelemetry는 레거시 Sleuth 설정을 벤더 중립적 옵저버빌리티로 대체- MDC는 서비스 간 추적 식별자를 자동으로 전파
neverBlock=true를 지정한 비동기 어펜더는 로깅이 요청 지연 시간에 영향을 주는 것을 방지- Logstash Logback Encoder 9.0은 Jackson 3.0과 Java 17 필수
- Spring Boot 4.1.1은 동시 스레드에 영향을 주는 중요한 JSON 인코딩 버그를 수정
- 민감 데이터 마스킹은 운영 로그에서 GDPR 준수를 보장
- 비동기 큐 용량 메트릭으로 로그 손실 전 알림 가능
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
Spring Boot 코드의 버그를 찾을 수 있나요
실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

작성자
Anthony Fillion-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 8월 22일 업데이트
태그
공유
관련 기사

Spring Boot Actuator: Micrometer와 Prometheus로 구현하는 운영 모니터링
운영 모니터링을 위한 Spring Boot Actuator 완전 가이드입니다. Micrometer 설정, Prometheus 지표, 커스텀 엔드포인트, 알림 구성을 다룹니다.

Spring Boot Observability 2026 완벽 가이드: OpenTelemetry, 분산 추적, 면접 질문
Spring Boot 3.4의 Observation API, OpenTelemetry 연동, 분산 추적 구현 방법을 상세히 설명합니다. Micrometer Tracing과 OTLP 내보내기 설정, 실무 면접 질문도 다룹니다.

Spring Boot YAML vs Properties: 설정 파일 비교 및 면접 질문 2026
Spring Boot의 YAML과 Properties 파일 차이점을 상세히 비교합니다. application.yml과 application.properties 선택 기준, 모범 사례, 기술 면접 대비 가이드를 다룹니다.