2026년 Spring Boot 구조화 로깅: Logback과 OpenTelemetry로 운영 JSON 출력

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

Logback과 JSON으로 구현하는 Spring Boot 구조화 로깅

전통적인 텍스트 로그는 운영 환경에서 빠르게 관리 불가능한 상태가 됩니다. 수백 개의 인스턴스가 초당 수천 줄의 로그를 생성하면 특정 오류를 찾는 일은 악몽이 됩니다. JSON 형식의 구조화 로그는 모든 이벤트를 쿼리 가능하고 자동으로 분석 가능하게 만들어 이러한 상황을 완전히 바꿉니다.

핵심 포인트

Spring Boot 4.x(Spring Framework 7 기반)는 ECS, Logstash, GELF 형식의 구조화된 JSON 로깅을 네이티브로 지원합니다. 새로운 spring-boot-starter-opentelemetry는 외부 의존성 없이 통합 옵저버빌리티를 제공합니다.

Spring Boot에서 구조화 로그를 도입하는 이유

전통적인 텍스트 로그의 한계

전형적인 텍스트 로그는 다음과 같은 모습입니다.

text
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으로 표현하면 즉시 활용할 수 있습니다.

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과 직접 통합됩니다.

yaml
# 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: DEBUG

ECS(Elastic Common Schema) 형식은 추가 설정 없이 Elasticsearch와 Kibana와의 직접적인 호환성을 보장합니다.

JSON 필드 커스터마이징

각 로그에 비즈니스 필드를 추가하기 위해 Spring Boot는 추가 속성 설정을 허용합니다.

yaml
# 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}
LoggingConfig.javajava
// 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를 도입하여 이전에 존재했던 복잡한 다중 의존성 설정을 대체했습니다. 이 단일 의존성은 트레이스, 메트릭, 로그 상관관계를 포함한 벤더 중립적 옵저버빌리티를 제공합니다.

xml
<!-- 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와 구조화 로그 설정

yaml
# 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 이상이 필수입니다.

xml
<!-- 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 파일은 출력 형식에 대한 완전한 제어를 제공합니다.

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)는 요청이나 추적 식별자 같은 컨텍스트 정보를 모든 로그에 추가합니다.

TracingFilter.javajava
// 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 사용

OrderService.javajava
// 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 로그에는 디버깅에 필요한 모든 정보가 포함됩니다.

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, 기술 테스트로 연습하세요.

성능을 위한 비동기 로깅

스레드 풀 설정

운영 환경에서 동기적인 로그 쓰기는 요청 지연 시간에 영향을 줍니다. 비동기 어펜더는 메인 스레드와 로깅을 분리합니다.

xml
<!-- 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 중요 버그 수정

Spring Boot 4.1.1에서는 JSON 인코딩 실패가 동일 스레드의 다음 로그 이벤트를 손상시킬 수 있는 심각한 문제(#51371)가 수정되었습니다. 조용한 로그 손상을 방지하려면 4.0.x 또는 4.1.0에서 업그레이드하십시오.

로깅 시스템 메트릭

로깅 시스템 자체를 모니터링하면 조용한 로그 손실을 방지할 수 있습니다.

LoggingMetrics.javajava
// 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로 전송합니다.

yaml
# 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 파이프라인

json
{
  "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 로깅 운영 환경 모범 사례

체계적으로 포함해야 할 정보

각 로그에는 디버깅과 상관관계 분석을 위한 최소 정보가 포함되어야 합니다.

StructuredLogger.javajava
// 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);
        }
    }
}
java
// 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()
    ));
}

제외해야 할 민감 정보

로그에는 절대로 개인 정보나 민감 데이터를 포함해서는 안 됩니다.

SensitiveDataFilter.javajava
// 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 구조에 대한 단위 테스트

StructuredLoggingTest.javajava
// 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.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-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 8월 22일 업데이트

태그

#spring boot logging
#logback json
#structured logs
#elk stack
#observability

공유

관련 기사