2.10 로깅 전략 (SLF4J / Logback)
스프링 부트는 SLF4J를 퍼사드로, 구현체로 Logback을 기본 사용합니다. 운영에서는 로그 레벨·형식·요청 추적(MDC) 을 정리해 두는 것이 중요합니다.
작성 기준: Spring Boot 3.2.x (spring-boot-starter-logging → Logback 포함)
1. SLF4J와 Logback 관계
- SLF4J (Simple Logging Facade for Java): 로그 API(퍼사드). 코드에서는
Logger를 통해 로그를 남깁니다. - Logback: SLF4J의 구현체. 스프링 부트는 spring-boot-starter-logging에 Logback을 포함하므로 별도 의존성 추가 없이 사용합니다.
- logback-classic: SLF4J 바인딩 + Logback 코어. logback-core: 실제 Appender·Encoder 등 구현.
애플리케이션 코드는 SLF4J만 사용하고, 어디에·어떤 형식으로 출력할지는 Logback 설정으로 제어합니다.
2. 기본 사용 (SLF4J)
@Slf4j // Lombok
@Service
public class OrderService {
public void create(OrderRequest req) {
log.info("주문 생성 요청: orderId={}", req.getOrderId());
// ...
log.debug("저장 완료: {}", order);
}
}
- 플레이스홀더로 문자열 결합을 피하면, 레벨이 꺼져 있을 때 불필요한 연산을 하지 않습니다.
3. application.yml로 레벨 설정
logging:
level:
root: INFO
com.example: DEBUG
org.springframework.web: INFO
org.hibernate.SQL: DEBUG # SQL 로그 (개발 시)
- 패키지별 레벨 지정 가능. 운영에서는 root: INFO, 필요 패키지만 DEBUG로 두는 경우가 많습니다.
4. MDC — 요청별 추적
한 요청을 요청 ID로 묶어 추적하려면 MDC(Mapped Diagnostic Context) 를 사용합니다.
@Component
public class RequestIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, ...) {
String requestId = request.getHeader("X-Request-Id");
if (requestId == null) {
requestId = UUID.randomUUID().toString();
}
MDC.put("requestId", requestId);
try {
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
logback-spring.xml에서 패턴에 %X{requestId}를 넣으면 로그에 요청 ID가 포함됩니다.
5. Logback 설정 파일 (logback-spring.xml)
설정 파일은 src/main/resources/logback-spring.xml (또는 logback.xml)에 둡니다. logback-spring.xml을 쓰면 스프링 부트가 확장 기능(springProfile, springProperty)을 제공합니다.
5.1 configuration 구조
- <appender>: 로그를 어디로 보낼지 정의 (콘솔, 파일, 소켓 등).
- <logger>: 특정 패키지/클래스의 레벨과 사용할 appender.
- <root>: 최상위 로거. 모든 로그의 기본 레벨과 appender.
5.2 기본 콘솔 + 패턴 예시
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %X{requestId} %-5level %logger{36} - %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
자주 쓰는 패턴 변수
| 변수 | 의미 |
|---|---|
| %d | 타임스탬프 |
| %thread | 스레드 이름 |
| %X{key} | MDC 값 |
| %-5level | 로그 레벨 (5자 맞춤) |
| %logger{36} | 로거 이름 (최대 36자) |
| %msg | 메시지 |
| %n | 줄바꿈 |
| %ex | 예외 스택 트레이스 |
5.3 로거 상속 관계 (Logger Hierarchy & Additivity)
Logback의 로거(Logger) 는 이름 기준 계층으로 상속됩니다. 로거 이름은 보통 패키지·클래스 경로와 같습니다 (예: com.example.service.OrderService).
계층 구조
- root (이름 없음): 최상위. 모든 로거의 부모.
- com: root의 자식
- com.example: com의 자식
- com.example.service: com.example의 자식
- com.example.service.OrderService: com.example.service의 자식
레벨 상속: 자식 로거에 level을 지정하지 않으면 부모의 레벨을 따릅니다. 부모도 없으면 root까지 올라가서 root의 레벨이 적용됩니다.
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
<!-- com.example 아래는 모두 INFO 상속. 단, com.example.service만 DEBUG로 오버라이드 -->
<logger name="com.example.service" level="DEBUG"/>
com.example.service.OrderService에서 남긴 로그 → DEBUG (com.example.service 로거 설정 사용)com.example.controller에서 남긴 로그 → INFO (root 상속)
additivity (Appender 상속)
기본값은 additivity="true" 입니다. 자식 로거에 appender를 붙이면, 그 로그는 자식 로거의 appender에 출력되고, 부모·조상 로거의 appender에도 그대로 전달됩니다. 그래서 같은 로그가 root의 CONSOLE에도 찍혀 중복 출력이 날 수 있습니다.
- additivity="false" 로 두면, 해당 로거에서 처리한 로그는 부모로 전달되지 않습니다. 특정 패키지만 별도 파일로 빼고 root 콘솔에는 안 나오게 할 때 사용합니다.
<logger name="com.example.service" level="DEBUG" additivity="false">
<appender-ref ref="SERVICE_FILE"/>
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
com.example.service패키지 로그는 SERVICE_FILE로만 가고, CONSOLE에는 가지 않습니다.com.example.controller로그는 additivity가 true(기본)이므로 root만 따르고 CONSOLE로만 갑니다.
정리: 레벨은 가장 가까운 조상에서 결정되고, appender는 additivity가 true일 때 자신 + 모든 조상 appender에 전달됩니다. 중복을 막으려면 additivity="false" 를 고려하세요.
5.4 로깅 설정 옵션 참조
실무에서 자주 쓰는 Logback·application.yml 옵션을 정리했습니다.
로그 레벨 (Level)
| 레벨 | 의미 | 사용 시점 |
|---|---|---|
| TRACE | 가장 상세 | 흐름 추적, 디버깅 초기 단계 (운영에서는 거의 사용 안 함) |
| DEBUG | 디버그 정보 | 개발 시 SQL·파라미터·내부 상태 확인 |
| INFO | 일반 정보 | 요청·처리 완료·주요 비즈니스 이벤트 (운영 기본) |
| WARN | 경고 | 복구 가능한 이상, deprecated 사용 등 |
| ERROR | 오류 | 예외·실패·복구 불가 상황 |
레벨은 해당 레벨 이상만 출력됩니다. level="INFO"이면 INFO, WARN, ERROR만 남고 DEBUG·TRACE는 제외됩니다.
ConsoleAppender 옵션
| 옵션 | 설명 | 예시 |
|---|---|---|
| encoder | 출력 형식 정의. 하위에 pattern, charset 사용 | 위 5.2 예시 참고 |
| pattern | 변환 패턴 문자열. %d, %level, %msg 등 조합 | %d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n |
| charset | 인코딩 | UTF-8 |
| target | 출력 대상. System.out(기본) / System.err | System.err |
RollingFileAppender 옵션
| 옵션 | 설명 | 예시 |
|---|---|---|
| file | 현재 쓰는 로그 파일 경로 | ${LOG_PATH}/app.log |
| encoder | 파일에 쓸 형식 (pattern, charset) | 동일 |
| rollingPolicy | 로테이션 정책 (아래 참고) | SizeAndTimeBasedRollingPolicy |
| append | 기존 파일에 이어쓸지 (기본 true) | true |
SizeAndTimeBasedRollingPolicy 옵션
| 옵션 | 설명 | 예시 |
|---|---|---|
| fileNamePattern | 롤링된 파일 이름. %d{...}(날짜), %i(인덱스) 사용 | app-%d{yyyy-MM-dd}.%i.log.gz |
| maxFileSize | 파일당 최대 크기. 초과 시 새 파일로 롤링 | 100MB, 500MB |
| maxHistory | 보관할 롤링 파일 개수(일 단위일 때는 일수) | 30 (30일) |
| totalSizeCap | 모든 롤링 파일 합계 상한. 초과 시 오래된 파일부터 삭제 | 3GB |
| cleanHistoryOnStart | 기동 시 오래된 파일 정리 (기본 false) | true |
TimeBasedRollingPolicy (일자만 기준)
일자만으로 롤링할 때 사용합니다. fileNamePattern에 %d{yyyy-MM-dd} 등만 넣고, maxHistory로 보관 일수를 제한합니다. maxFileSize는 없습니다.
Encoder (PatternLayoutEncoder) 옵션
| 옵션 | 설명 | 예시 |
|---|---|---|
| pattern | 변환 패턴 | %d %level [%thread] %logger - %msg%n |
| charset | 문자 인코딩 | UTF-8 |
| immediateFlush | 매 로그마다 플러시 (기본 true). false면 버퍼링 | true |
패턴 변수 추가 참고
| 변수 | 설명 |
|---|---|
| %d{format} | 날짜. format 예: yyyy-MM-dd HH:mm:ss.SSS, ISO8601 |
| %relative | 기동 후 경과 밀리초 |
| %level / %-5level | 레벨 (숫자는 최소 너비, -는 왼쪽 정렬) |
| %logger{length} | 로거 이름. %logger{36}이면 FQCN 36자로 축약 |
| %class / %method / %line | 호출 클래스·메서드·라인 (비용 있음) |
| %ex / %throwable | 예외 스택. %ex{short}면 한 줄 요약 |
| %X{key} | MDC 값. %X만 쓰면 전체 MDC |
AsyncAppender 옵션
| 옵션 | 설명 | 기본값 | 비고 |
|---|---|---|---|
| queueSize | 대기 큐 크기 | 256 | 넘치면 discardingThreshold 적용 |
| discardingThreshold | 큐 잔량이 이 비율(%) 미만이면 TRACE/DEBUG/INFO 버림. 0이면 레벨 구분 없이 버림 | 20 | 0이면 모두 버릴 수 있음 |
| includeCallerData | 호출 위치(클래스·라인) 포함 여부. 비동기에서는 비용 큼 | false | 필요 시만 true |
| neverBlock | 큐 가득 찼을 때 블로킹 대신 로그 드롭 | false | true면 드롭, false면 블로킹 |
| appender-ref | 실제로 쓰는 하위 appender | — | 1개만 지정 |
application.yml — logging 옵션
| 키 | 설명 | 예시 |
|---|---|---|
| logging.level.root | root 로거 레벨 | INFO |
| logging.level.패키지 | 패키지별 레벨 | com.example: DEBUG |
| logging.file.name | 로그 파일 경로 (이름까지) | logs/app.log |
| logging.file.path | 로그 디렉터리 (파일명은 spring.log) | logs |
| logging.pattern.console | 콘솔 출력 패턴 | %d %-5level %logger{36} - %msg%n |
| logging.pattern.file | 파일 출력 패턴 | 동일 |
| logging.pattern.dateformat | %d 기본 날짜 형식 | yyyy-MM-dd HH:mm:ss.SSS |
logback-spring.xml이 있으면 logging.pattern·logging.file 등은 XML 설정이 우선합니다. 레벨만 yml로 두고 패턴·appender는 XML에서 관리하는 조합이 많습니다.
6. 실전: RollingFileAppender (파일 로테이션)
운영에서는 파일로 남기고, 용량·일자 기준으로 로테이션하는 경우가 많습니다.
<property name="LOG_PATH" value="logs"/>
<property name="LOG_FILE" value="application"/>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_FILE}.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_FILE}-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
</appender>
- SizeAndTimeBasedRollingPolicy: 일자 + 크기 기준. 같은 날 100MB 넘으면 %i로 새 파일.
- maxHistory: 보관할 일수 (30일).
- totalSizeCap: 전체 로그 용량 상한.
7. 프로파일별 설정 (springProfile)
개발/운영에서 다른 appender·레벨을 쓰려면 springProfile로 분기합니다.
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<logger name="com.example" level="INFO"/>
</springProfile>
8. springProperty — application.yml 값 참조
설정값을 application.yml에서 가져와 로그 경로·파일명에 쓸 수 있습니다.
<springProperty scope="context" name="LOG_PATH" source="logging.file.path" defaultValue="logs"/>
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="app"/>
- source: application.yml의 키. defaultValue: 없을 때 사용할 값.
9. AsyncAppender — 비동기 출력
로그 I/O가 많을 때 AsyncAppender로 버퍼에 쌓았다가 비동기로 전달하면 요청 스레드 지연을 줄일 수 있습니다.
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="FILE"/>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/>
</root>
- queueSize: 대기 큐 크기. 넘치면 discardingThreshold 비율만큼 TRACE/DEBUG부터 버립니다 (0이면 다 버림).
- appender-ref: 실제로 로그를 쓰는 하위 appender (FILE 등).
10. JSON 로그 (실전 수집·검색)
ELK, CloudWatch, Datadog 등으로 수집·검색할 때는 JSON 한 줄(one JSON per line)이 유리합니다. logstash-logback-encoder를 쓰면 패턴 대신 JSON으로 출력할 수 있습니다.
implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/app-json.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/app-json-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>requestId</includeMdcKeyName>
<includeMdcKeyName>userId</includeMdcKeyName>
</encoder>
</appender>
- LogstashEncoder: 타임스탬프·레벨·로거·메시지·예외·MDC를 JSON 필드로 출력합니다.
- includeMdcKeyName: MDC 키를 JSON 필드로 포함. 요청 ID·사용자 ID 등을 검색에 활용할 수 있습니다.
11. MDC 고급 활용
- 요청 ID: 필터/인터셉터에서
MDC.put("requestId", id)후 finally에서 MDC.clear(). - 사용자 ID: 인증 후
MDC.put("userId", userId). - 트랜잭션 ID: 필요 시 서비스 계층에서 설정.
자식 스레드(@Async, Executor)에서는 MDC가 자동 상속되지 않으므로, TaskDecorator로 부모 MDC를 복사해 넘기는 패턴을 사용합니다.
12. 운영 시 주의사항
- 비밀번호·토큰·카드 번호 등은 로그에 남기지 않고, 필요 시 마스킹합니다.
- 스택 트레이스는 ERROR 위주로 남기고, 로그 양이 많으면 파일 로테이션과 보존 기간(maxHistory, totalSizeCap)을 설정합니다.
- JSON 포맷으로 출력하면 ELK·CloudWatch 등으로 수집·검색하기 쉽습니다.
- AsyncAppender 사용 시 큐가 가득 차면 로그가 버려질 수 있으므로, queueSize와 discardingThreshold를 부하에 맞게 조정합니다.
개발 시에는 SQL·파라미터를 DEBUG로 보다가, 운영에서는 root: INFO로 두고 예외·핵심 비즈니스만 남기는 구분을 권장합니다. Logback은 logback-spring.xml 한 파일에서 콘솔/파일/JSON/프로파일을 모두 제어할 수 있습니다.