Spring Boot 구조화 로깅이란?
Spring Boot 3.4에서 도입된 Structured Logging은 로그를 JSON 등 기계가 파싱 가능한 형식으로 출력하는 기능입니다. 기존에는 Logback의 JsonLayout이나 logstash-logback-encoder 같은 서드파티 라이브러리가 필요했지만, 이제 한 줄 설정으로 ECS(Elastic Common Schema), Logstash, GELF 형식의 JSON 로그를 출력할 수 있습니다.
한 줄 설정으로 JSON 로그 활성화
# application.yml — Spring Boot 3.4+
# ECS 형식 (Elasticsearch/Kibana 호환)
logging:
structured:
format:
console: ecs
file: ecs
# Logstash 형식
logging:
structured:
format:
console: logstash
# 콘솔은 사람이 읽기 쉽게, 파일은 JSON으로
logging:
structured:
format:
console: # 비워두면 기본 텍스트 포맷
file: ecs
file:
name: /var/log/app/application.log
// 일반 로그 출력
@Slf4j
@Service
public class OrderService {
public Order createOrder(CreateOrderRequest request) {
log.info("주문 생성 시작: userId={}, amount={}",
request.getUserId(), request.getAmount());
Order order = orderRepository.save(toEntity(request));
log.info("주문 생성 완료: orderId={}", order.getId());
return order;
}
}
// ECS 형식 출력 결과:
// {
// "@timestamp": "2026-03-20T17:00:00.123Z",
// "log.level": "INFO",
// "process.pid": 12345,
// "process.thread.name": "http-nio-8080-exec-1",
// "service.name": "order-service",
// "service.version": "1.2.0",
// "service.environment": "production",
// "log.logger": "com.example.OrderService",
// "message": "주문 생성 시작: userId=42, amount=99000"
// }
ECS vs Logstash vs GELF: 형식 비교
| 형식 | 호환 시스템 | 특징 |
|---|---|---|
ecs |
Elasticsearch, Kibana, Filebeat | Elastic Common Schema 표준, 필드명 계층 구조 |
logstash |
Logstash, ELK 스택 | logstash-logback-encoder 호환 형식 |
gelf |
Graylog | Graylog Extended Log Format |
Fluent API: 구조화된 키-값 로깅
Spring Boot 3.4의 핵심 기능인 Fluent Logging API는 로그 메시지에 구조화된 키-값 쌍을 추가합니다. JSON 출력 시 별도 필드로 분리되어 검색과 필터링이 가능합니다:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static net.logstash.logback.argument.StructuredArguments.*;
@Slf4j
@Service
public class PaymentService {
public PaymentResult processPayment(PaymentRequest request) {
// SLF4J 2.0 Fluent API + 구조화 필드
log.atInfo()
.setMessage("결제 처리 시작")
.addKeyValue("orderId", request.getOrderId())
.addKeyValue("amount", request.getAmount())
.addKeyValue("currency", request.getCurrency())
.addKeyValue("paymentMethod", request.getMethod())
.log();
try {
PaymentResult result = gateway.charge(request);
log.atInfo()
.setMessage("결제 성공")
.addKeyValue("orderId", request.getOrderId())
.addKeyValue("transactionId", result.getTransactionId())
.addKeyValue("processingTimeMs", result.getDurationMs())
.log();
return result;
} catch (PaymentDeclinedException e) {
log.atWarn()
.setMessage("결제 거절")
.addKeyValue("orderId", request.getOrderId())
.addKeyValue("declineReason", e.getReason())
.addKeyValue("declineCode", e.getCode())
.setCause(e)
.log();
throw e;
}
}
}
// JSON 출력 (ECS 형식):
// {
// "@timestamp": "2026-03-20T17:00:01.456Z",
// "log.level": "INFO",
// "message": "결제 처리 시작",
// "orderId": "ORD-2026-001234",
// "amount": 99000,
// "currency": "KRW",
// "paymentMethod": "CARD",
// "service.name": "payment-service"
// }
커스텀 필드: 서비스 메타데이터 자동 추가
# application.yml — 모든 로그에 공통 필드 추가
logging:
structured:
format:
console: ecs
ecs:
service:
name: order-service
version: 1.2.0
environment: production
node-name: ${HOSTNAME:unknown}
spring:
application:
name: order-service # service.name 기본값으로 사용됨
// 커스텀 StructuredLoggingJsonMembersCustomizer로 필드 추가
@Component
public class CustomJsonFieldsCustomizer
implements StructuredLoggingJsonMembersCustomizer<ILoggingEvent> {
@Override
public void customize(JsonMembers<ILoggingEvent> members) {
// 모든 로그에 deployment 정보 추가
members.add("deploy.region", "ap-northeast-2");
members.add("deploy.cluster", "prod-eks-01");
// 동적 필드: 이벤트에서 추출
members.add("trace.id", event ->
MDC.get("traceId"));
members.add("span.id", event ->
MDC.get("spanId"));
// 기존 필드 이름 변경
members.rename("log.level", "severity");
// 불필요한 필드 제거
members.exclude("process.pid");
}
}
MDC + 구조화 로깅: 요청 추적
MDC(Mapped Diagnostic Context)의 값은 구조화 로그에 자동으로 포함됩니다:
@Component
public class RequestTracingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String traceId = Optional.ofNullable(request.getHeader("X-Trace-Id"))
.orElse(UUID.randomUUID().toString());
String requestId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
MDC.put("requestId", requestId);
MDC.put("method", request.getMethod());
MDC.put("uri", request.getRequestURI());
MDC.put("clientIp", request.getRemoteAddr());
try {
chain.doFilter(request, response);
MDC.put("statusCode", String.valueOf(response.getStatus()));
log.atInfo()
.setMessage("요청 처리 완료")
.addKeyValue("responseTimeMs", getElapsedMs())
.log();
} finally {
MDC.clear();
}
}
}
// JSON 출력 — MDC 값이 자동 포함:
// {
// "@timestamp": "2026-03-20T17:00:02.789Z",
// "message": "요청 처리 완료",
// "traceId": "abc-123-def",
// "requestId": "req-456",
// "method": "POST",
// "uri": "/api/orders",
// "clientIp": "10.0.0.5",
// "statusCode": "201",
// "responseTimeMs": 45
// }
Micrometer Observation + 구조화 로깅
Micrometer Observation API와 결합하면 분산 추적 ID가 자동으로 로그에 포함됩니다:
# application.yml
management:
tracing:
sampling:
probability: 1.0
observations:
key-values:
application: order-service
logging:
structured:
format:
console: ecs
pattern:
correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
@RestController
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final ObservationRegistry observationRegistry;
@PostMapping("/api/orders")
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest req) {
return Observation.createNotStarted("order.create", observationRegistry)
.lowCardinalityKeyValue("order.type", req.getType())
.highCardinalityKeyValue("user.id", req.getUserId())
.observe(() -> {
// 이 블록 내 모든 로그에 traceId, spanId 자동 포함
Order order = orderService.createOrder(req);
return ResponseEntity.status(201).body(order);
});
}
}
// 출력: traceId와 spanId가 자동 주입
// {
// "@timestamp": "2026-03-20T17:00:03.012Z",
// "message": "주문 생성 완료",
// "trace.id": "6f3b2c1a8e9d4f5b",
// "span.id": "a1b2c3d4e5f6",
// "orderId": "ORD-001234",
// "order.type": "standard"
// }
로그 민감 정보 마스킹
@Component
public class SensitiveDataMaskingCustomizer
implements StructuredLoggingJsonMembersCustomizer<ILoggingEvent> {
private static final Pattern EMAIL_PATTERN =
Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}");
private static final Pattern CARD_PATTERN =
Pattern.compile("\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b");
@Override
public void customize(JsonMembers<ILoggingEvent> members) {
// 메시지 내 민감 정보 마스킹
members.add("message", event -> {
String msg = event.getFormattedMessage();
msg = EMAIL_PATTERN.matcher(msg)
.replaceAll(m -> maskEmail(m.group()));
msg = CARD_PATTERN.matcher(msg)
.replaceAll("****-****-****-$4");
return msg;
});
}
private String maskEmail(String email) {
int at = email.indexOf('@');
if (at <= 2) return "***" + email.substring(at);
return email.substring(0, 2) + "***" + email.substring(at);
}
}
// 입력: "결제 완료: user@example.com, 카드 1234-5678-9012-3456"
// 출력: "결제 완료: us***@example.com, 카드 ****-****-****-3456"
프로파일별 로깅 전략
# application.yml — 기본 (개발)
logging:
structured:
format:
console: # 빈 값 → 텍스트 포맷 (읽기 쉬움)
---
# application-prod.yml — 프로덕션
logging:
structured:
format:
console: ecs
file: ecs
file:
name: /var/log/app/application.log
level:
root: WARN
com.example: INFO
---
# application-staging.yml — 스테이징
logging:
structured:
format:
console: logstash # Logstash로 직접 전송 시
level:
root: INFO
com.example: DEBUG
테스트에서 구조화 로그 검증
@SpringBootTest
class OrderServiceLoggingTest {
@Autowired
private OrderService orderService;
@RegisterExtension
static OutputCaptureExtension output = new OutputCaptureExtension();
@Test
void shouldLogStructuredOrderCreation(CapturedOutput capture) {
// given
var request = new CreateOrderRequest("user-42", BigDecimal.valueOf(99000));
// when
orderService.createOrder(request);
// then — 구조화 로그 필드 검증
String log = capture.getAll();
assertThat(log).contains(""orderId":");
assertThat(log).contains(""message":"주문 생성 완료"");
// JSON 파싱으로 정확한 필드 검증
ObjectMapper mapper = new ObjectMapper();
String jsonLine = Arrays.stream(log.split("n"))
.filter(l -> l.contains("주문 생성 완료"))
.findFirst()
.orElseThrow();
JsonNode node = mapper.readTree(jsonLine);
assertThat(node.get("orderId").asText()).isNotEmpty();
assertThat(node.get("log.level").asText()).isEqualTo("INFO");
}
}
핵심 정리
| 기능 | 설정 |
|---|---|
| JSON 로그 활성화 | logging.structured.format.console: ecs |
| 구조화 키-값 추가 | log.atInfo().addKeyValue("k","v").log() |
| 커스텀 필드 추가 | StructuredLoggingJsonMembersCustomizer |
| 분산 추적 연동 | Micrometer Observation → traceId/spanId 자동 포함 |
| 민감정보 마스킹 | Customizer에서 message 필드 변환 |
Spring Boot 3.4 Structured Logging은 서드파티 없이 한 줄 설정으로 프로덕션급 JSON 로깅을 제공합니다. ELK, Grafana Loki 등 로그 수집 시스템과 즉시 호환되며, Fluent API로 검색 가능한 구조화 필드를 추가할 수 있습니다. Spring Logback MDC 구조화 로깅과 Spring Observation API 심화도 함께 참고하세요.