Spring Boot 로깅 기본: SLF4J + Logback 빠르게 세팅 (레벨/패턴/MDC/마스킹)
Spring Boot 3에서 SLF4J+Logback으로 로그 레벨과 패턴을 정리하고, MDC로 요청 추적을 붙인 뒤 민감정보 마스킹 포인트까지 실무 기준으로 빠르게 세팅합니다.
도입 (문제 상황)
운영 장애가 났는데 로그에 “뭔가”는 찍혀 있지만, 정작 어떤 요청이었는지 추적이 안 될 때가 있습니다. 반대로 로그를 자세히 찍었더니 비밀번호/토큰 같은 민감정보가 섞여서 보안 이슈가 되기도 해요. 이번 글에서는 Spring Boot에서 SLF4J + Logback을 “딱 실무에서 필요한 만큼” 빠르게 세팅해 봅니다.
핵심 개념 — Spring Boot 로깅에서 꼭 잡아야 하는 4가지
Spring Boot 3.x는 기본 로깅 구현체로 Logback을 사용하고, 애플리케이션 코드는 SLF4J API로 로깅하는 구성이 표준입니다. 중요한 건 “로깅 프레임워크를 아는 것”보다, 아래 4가지를 일관된 기준으로 잡는 일이에요.
1) 로그 레벨: “정보”가 아니라 “의도”로 나누기
레벨은 단순히 많고 적음이 아니라 운영에서 어떤 액션을 유도하는지가 기준입니다.
ERROR: 즉시 조치 필요(알람 대상). 예외 스택트레이스 포함이 일반적WARN: 지금은 동작하지만 위험 신호(리트라이/타임아웃/외부 의존성 불안정)INFO: 비즈니스 흐름의 주요 이벤트(서버 시작, 배치 완료, 주문 생성 등)DEBUG/TRACE: 원인 분석용(운영 상시 ON은 비용 큼)
특히 org.springframework나 org.hibernate 같은 프레임워크 로그는 필요할 때만 올리도록 패키지별 레벨을 분리해 두는 게 좋습니다.
2) 로그 패턴: “읽기 쉬움” + “머신이 파싱하기 쉬움”
운영에서는 사람이 보는 콘솔 로그도 중요하지만, 대부분은 수집(ELK/Cloud Logging 등)을 전제로 합니다. 그래서 패턴에는 보통 다음을 포함합니다.
- 시간(밀리초 포함), 레벨, 스레드, 로거명
- 요청 추적 키(예:
traceId,requestId) → 이게 없으면 “로그는 많은데 연결이 안 됨” 상태가 됩니다.
3) MDC 맛보기: 요청 단위로 로그를 묶는 “꼬리표”

MDC(Mapped Diagnostic Context)는 “현재 스레드 컨텍스트에 키-값을 붙여서” 로그 패턴에서 꺼내 쓰는 방식입니다.
비유하자면, 택배 상자(로그)마다 송장번호(traceId) 스티커를 붙여 두는 느낌이에요. 그러면 여러 줄에 흩어진 로그도 같은 송장번호로 검색해서 한 번에 모을 수 있습니다.
주의할 점은 요청이 끝나면 반드시 MDC를 비워야 한다는 겁니다. 안 비우면 스레드 재사용(톰캣 스레드풀) 때문에 다른 요청 로그에 섞일 수 있어요.
4) 민감정보 마스킹: “로그를 줄이는 것”보다 “어디서 새는지”를 막기
마스킹은 만능이 아닙니다. 가장 효과적인 전략은 보통 이 순서예요.
- 애초에 민감정보를 로그에 남기지 않기(DTO
toString()/request body 로깅 금지) - 불가피한 경우에만 필터/컨버터/로거에서 마스킹
- 마지막 보루로 Logback 레이아웃/필터에서 패턴 기반 마스킹(오탐/누락 가능성 있음)
아래 코드 예제에서는 “실무에서 최소한으로 안전장치를 거는” 수준으로, 요청 ID(MDC) + Authorization 헤더 마스킹 포인트를 함께 잡아봅니다.
코드 예제 — Spring Boot 3에서 Logback 패턴 + MDC + 헤더 마스킹
아래 예제는 그대로 복붙해서 실행할 수 있는 구성입니다.
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// 기본으로 Logback 포함 (spring-boot-starter-logging)
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
application.yml (레벨 설정)
spring:
application:
name: logging-demo
logging:
level:
root: INFO
com.example: DEBUG
org.springframework.web: INFO
org.hibernate.SQL: WARN
logback-spring.xml (패턴 + MDC 출력)
src/main/resources/logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Spring Boot 기본값 일부 재사용 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 콘솔 로그 패턴: traceId/requestId를 MDC에서 꺼내 출력 -->
<property name="CONSOLE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - traceId=%X{traceId:-} requestId=%X{requestId:-} %msg%n%ex"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
%X{traceId:-}: MDC에traceId가 없으면-출력%ex: 예외 스택트레이스(운영 분석에 매우 중요)
MDC를 심는 Filter (요청 시작/종료에서 세팅/정리)
src/main/java/com/example/loggingdemo/logging/MdcFilter.java
package com.example.loggingdemo.logging;
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.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
@Component
public class MdcFilter extends OncePerRequestFilter {
private static final String TRACE_ID = "traceId";
private static final String REQUEST_ID = "requestId";
private static final String HEADER_REQUEST_ID = "X-Request-Id";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 외부에서 X-Request-Id를 주면 재사용, 없으면 생성
String requestId = Optional.ofNullable(request.getHeader(HEADER_REQUEST_ID))
.filter(v -> !v.isBlank())
.orElse(UUID.randomUUID().toString());
// traceId는 단순 예시로 UUID 사용 (실무에선 분산추적 도구/게이트웨이 연동 고려)
String traceId = UUID.randomUUID().toString();
MDC.put(REQUEST_ID, requestId);
MDC.put(TRACE_ID, traceId);
// 응답에도 실어주면 클라이언트/게이트웨이에서 추적이 쉬워집니다.
response.setHeader(HEADER_REQUEST_ID, requestId);
try {
filterChain.doFilter(request, response);
} finally {
// 중요: 스레드 재사용 때문에 반드시 정리
MDC.clear();
}
}
}
민감정보 마스킹 포인트: “헤더 로깅”을 한다면 여기서 안전하게
아래는 요청 로그를 남길 때 Authorization 헤더는 토큰 전체를 찍지 않도록 마스킹하는 예시입니다.
(참고로, 요청 바디까지 찍는 로깅은 운영에서 위험/비용이 커서 신중히 선택하시는 편이 좋습니다.)
src/main/java/com/example/loggingdemo/web/RequestLoggingInterceptor.java
package com.example.loggingdemo.web;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Optional;
@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(RequestLoggingInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String auth = Optional.ofNullable(request.getHeader("Authorization")).orElse("");
String maskedAuth = maskAuthHeader(auth);
log.info("HTTP {} {} auth={}",
request.getMethod(),
request.getRequestURI(),
maskedAuth);
return true;
}
private String maskAuthHeader(String auth) {
if (auth == null || auth.isBlank()) return "-";
// 예: "Bearer eyJhbGciOi..." -> "Bearer ***"
if (auth.toLowerCase().startsWith("bearer ")) return "Bearer ***";
// 그 외는 길이만 남기는 정도의 보수적 마스킹
return "***(" + auth.length() + ")";
}
}
Interceptor 등록
src/main/java/com/example/loggingdemo/web/WebConfig.java
package com.example.loggingdemo.web;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final RequestLoggingInterceptor requestLoggingInterceptor;
public WebConfig(RequestLoggingInterceptor requestLoggingInterceptor) {
this.requestLoggingInterceptor = requestLoggingInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestLoggingInterceptor)
.addPathPatterns("/**");
}
}
테스트용 Controller
src/main/java/com/example/loggingdemo/web/HelloController.java
package com.example.loggingdemo.web;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
private static final Logger log = LoggerFactory.getLogger(HelloController.class);
@GetMapping("/hello")
public String hello() {
log.debug("Hello endpoint called");
return "ok";
}
}
실행 후:
curl -H "Authorization: Bearer secret-token-value" http://localhost:8080/hello
로그에서 traceId=... requestId=...가 항상 붙고, auth=Bearer ***로 마스킹되는지 확인해 보세요.
flowchart LR
A["Client"] --> B["Servlet Filter: MdcFilter"]
B --> C["Spring MVC: DispatcherServlet"]
C --> D["HandlerInterceptor: RequestLoggingInterceptor"]
D --> E["Controller"]
E --> F["Logback Pattern: %X{traceId} %X{requestId}"]
요청이 들어오면 Filter에서 MDC를 세팅하고, 이후 모든 로그가 같은 traceId/requestId로 묶입니다.
(정리) 무엇을 어디서 설정할지 한눈에 보기
| 항목 | 권장 위치 | 이유 | 주의점 |
|---|---|---|---|
| 로그 레벨(root/패키지별) | application.yml |
환경별로 쉽게 조절 | DEBUG 상시 운영은 비용 큼 |
| 로그 패턴(MDC 포함) | logback-spring.xml |
포맷을 중앙에서 통제 | 과도한 정보 출력 금지 |
| MDC 세팅/정리 | OncePerRequestFilter |
요청 시작/종료가 명확 | MDC.clear() 누락 주의 |
| 민감정보 마스킹 | “로그를 남기는 지점(Interceptor/Service)” | 가장 정확하고 안전 | 레이아웃 기반 마스킹은 오탐/누락 가능 |
실무 팁
💡 실무에서는: “민감정보가 찍히는 지점”부터 차단해 보세요
request.toString(), DTO 자동toString()(Lombok@ToString)이 의외로 사고를 많이 냅니다. 비밀번호/주민번호/토큰 필드는@ToString.Exclude같은 방식으로 원천 차단하는 게 가장 안전합니다.- Spring Security를 쓰는 경우에도 인증 관련 객체를 통째로 로그로 찍지 말고, 필요한 식별자(예: userId)만 선별해서 남기는 편이 좋습니다.
💡 실무에서는: traceId 전략을 “팀 표준”으로 정해두면 검색 비용이 확 줄어요
- 게이트웨이/로드밸런서가
X-Request-Id를 내려주면 애플리케이션은 그 값을 우선 사용하는 편이 좋습니다. 그래야 앞단(게이트웨이) 로그와 뒷단(애플리케이션) 로그가 같은 키로 연결됩니다. - 분산 추적을 도입할 계획이 있다면, UUID를 직접 만들기보다 OpenTelemetry 같은 표준과의 연동을 염두에 두고 키 이름(
traceId)과 전파 헤더 정책을 정해두세요.
핵심 요약: 로그 레벨은 “운영 액션 기준”으로 나누고, 패턴에는 traceId/requestId를 꼭 포함하세요.
핵심 요약: MDC는 Filter에서 세팅하고 요청 종료 시 반드시 MDC.clear()로 정리해야 합니다.
핵심 요약: 민감정보는 “마스킹”보다 “처음부터 로그에 안 남기기”가 더 안전합니다.
다음 글: #07 Bean과 DI(의존성 주입) 핵심만