Java 17 기준으로 SLF4J + Logback 조합에서 로그 레벨을 일관되게 운영하는 방법과 실무에서 자주 터지는 로깅 안티패턴을 정리합니다.
도입 (문제 상황)
장애가 났을 때 로그를 열어보면, 정작 필요한 정보는 없고 비슷한 문장만 수천 줄 쌓여 있는 경험이 있으실 거예요. 반대로 로그가 너무 조용해서 “왜 실패했는지”를 재현해야만 알 수 있는 경우도 자주 생깁니다. 로깅은 많이 찍는 기술이 아니라, 필요한 순간에 정확히 찾을 수 있게 남기는 기술입니다.
핵심 개념 (Java 로깅 전략의 기준점: SLF4J + Logback)

Java에서 로깅 전략을 세울 때 가장 먼저 정해야 하는 건 “어떤 API로 호출하고, 어떤 구현체로 출력할지”입니다. 실무에서는 SLF4J(파사드) + Logback(구현체) 조합이 사실상 표준처럼 쓰입니다. 코드에서는 SLF4J로만 로깅하고, 운영 환경에서 Logback 설정으로 포맷/레벨/출력 대상을 통제할 수 있어요. 즉, 로깅을 “코드 변경”이 아니라 “설정 변경”으로 운영할 수 있는 구조가 됩니다.
SLF4J + Logback이 중요한 이유
- 벤더 종속 최소화: 코드가 Logback API에 묶이지 않고 SLF4J에만 의존합니다.
- 성능과 안정성: Logback은 성숙한 설정/롤링 정책을 제공하고, 운영에서 검증된 조합입니다.
- 관측 가능성(Observability) 기반: “로그는 사건의 증거”입니다. 요청 단위로 추적 가능한 필드(예: requestId)를 남기면, 장애 분석 시간이 급격히 줄어듭니다.
로그 레벨을 “팀 규칙”으로 만드는 가이드
로그 레벨은 개인 취향이 아니라 운영 비용(스토리지/검색/알람 피로도) 과 직결됩니다. 아래처럼 기준을 정해두면 흔들림이 줄어들어요.
| 레벨 | 의미(의도) | 언제 쓰면 좋은가 | 주의점 |
|---|---|---|---|
| ERROR | 요청/기능이 실패했고 즉시 대응이 필요 | 예외로 인해 처리 실패, 데이터 손상 가능 | 너무 많이 찍히면 알람이 무력화됩니다 |
| WARN | 실패는 아니지만 이상 징후 | 재시도 성공, 외부 의존성 지연, fallback 동작 | “경고지만 정상”을 남발하면 신뢰도가 떨어집니다 |
| INFO | 서비스 운영에 필요한 주요 이벤트 | 서버 시작/종료, 배치 시작/종료, 주요 상태 변화 | 모든 요청/응답을 INFO로 남기면 비용 폭증 |
| DEBUG | 개발/분석용 상세 흐름 | 분기/파라미터/중간 결과 확인 | 운영 기본 레벨로 두지 않는 게 일반적 |
| TRACE | 매우 상세(루프/내부 단계) | 짧은 시간, 특정 문제 재현에 한정 | 성능/용량에 가장 치명적 |
추상적으로 보면 “INFO는 사건의 요약, DEBUG는 사건의 과정, ERROR는 사건의 실패”라고 생각하시면 편합니다. 마치 CCTV가 24시간 모든 프레임을 저장하는 게 아니라, 사건이 발생했을 때 필요한 장면을 빠르게 찾도록 인덱싱하는 것과 비슷해요.
로깅 안티패턴 (운영에서 진짜 문제 되는 것들)
- 문자열 더하기로 로그 만들기
logger.debug("value=" + expensiveCall())같은 코드는 레벨이 꺼져 있어도 문자열 결합/메서드 호출이 실행될 수 있습니다.- SLF4J의
{}플레이스홀더를 쓰면, 해당 레벨이 비활성일 때 불필요한 비용을 줄일 수 있어요.
- 예외 스택트레이스를 누락
logger.error("failed: {}", e.getMessage())만 남기면 “어디서 터졌는지”가 사라집니다.- 예외는 마지막 인자로 넘겨 스택트레이스를 반드시 남기는 게 원칙입니다.
- 로그에 민감정보(PII/토큰/비밀번호) 출력
- 운영 로그는 생각보다 많은 사람이 접근합니다(권한이 있는 사람, 외부 위탁, 백업 등).
- 마스킹/필터링을 기본값으로 두고, 필요한 경우에만 제한적으로 출력해야 합니다.
- 요청 단위 상관관계(correlation) 없이 로그를 흩뿌리기
- 멀티스레드/비동기 환경에서는 같은 요청의 로그가 섞입니다.
MDC로requestId같은 키를 심어두면 검색이 “한 번에” 됩니다.
flowchart LR
A["Request Start"] --> B["MDC put requestId"]
B --> C["Business Logs (INFO/DEBUG)"]
C --> D["Error occurs?"]
D -->|Yes| E["logger.error with exception"]
D -->|No| F["Normal response"]
E --> G["MDC clear"]
F --> G["MDC clear"]
요청 시작 시 MDC를 세팅하고 종료 시 정리하면, 로그를 requestId로 손쉽게 묶을 수 있습니다.
코드 예제 (SLF4J + Logback 설정과 MDC까지 한 번에)
아래 예제는 Java 17 + Maven 기준이며, 그대로 복붙해서 실행할 수 있습니다. 핵심은 (1) SLF4J API로만 로깅 (2) Logback 설정으로 출력 제어 (3) MDC로 requestId를 자동 주입하는 흐름입니다.
실행 방법:
mvn -q -DskipTests package && java -jar target/logging-demo-1.0.0.jar
1) pom.xml
<!-- pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>blog</groupId>
<artifactId>logging-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencies>
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<!-- Logback implementation (SLF4J 2.x compatible) -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<mainClass>blog.LoggingDemoApp</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
2) logback.xml (src/main/resources/logback.xml)
<!-- src/main/resources/logback.xml -->
<configuration>
<!-- 운영에서는 INFO, 문제 분석 시 DEBUG로 올리는 식으로 환경별로 분리하는 편이 좋습니다 -->
<property name="LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} requestId=%X{requestId} - %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 파일 롤링(일 단위 + 용량 제한) -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>7</maxHistory>
<totalSizeCap>200MB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<!-- 특정 패키지만 DEBUG로 (필요할 때만) -->
<logger name="blog" level="DEBUG"/>
</configuration>
3) 실행 코드 (src/main/java/blog/LoggingDemoApp.java)
package blog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.UUID;
public class LoggingDemoApp {
private static final Logger log = LoggerFactory.getLogger(LoggingDemoApp.class);
public static void main(String[] args) {
// 요청 시작(예: HTTP 요청이 들어왔다고 가정)
String requestId = UUID.randomUUID().toString().substring(0, 8);
MDC.put("requestId", requestId);
try {
log.info("request started");
String userId = "user-123";
log.debug("load user profile. userId={}", userId);
// 안티패턴 방지: 문자열 결합 대신 {} 사용
int result = expensiveCalculation(10);
log.info("calculation done. result={}", result);
// 예외 로깅: 스택트레이스를 반드시 남김
simulateFailure();
log.info("request finished"); // 여기까지 오면 정상 종료
} catch (Exception e) {
log.error("request failed", e);
} finally {
// 스레드 재사용(스레드풀) 환경에서 MDC 누수 방지
MDC.clear();
}
}
private static int expensiveCalculation(int input) {
// 예시용: 실제로는 DB/외부 API/복잡한 계산 등이 들어갈 수 있습니다.
return input * 42;
}
private static void simulateFailure() {
throw new IllegalStateException("downstream service timeout");
}
}
실무 팁
💡 실무에서는: “로그 레벨”을 배포가 아니라 운영 설정으로 바꾸게 해두세요
- 장애 대응 중에 가장 많이 하는 행동이 “일시적으로 DEBUG를 올려서 증거를 더 모으는 것”입니다.
- 애플리케이션 재배포 없이 레벨을 바꿀 수 있도록(예: 설정 파일/환경변수/관리 엔드포인트) 운영 전략을 잡아두면 MTTR이 줄어듭니다.
- 단, DEBUG를 장시간 켜두면 비용이 급증하니 시간 제한과 대상 패키지 제한을 같이 두는 편이 안전합니다.
💡 실무에서는: “무엇을 남길지” 체크리스트를 먼저 만들면 로그가 정돈됩니다
- 성공 로그:
무엇을 처리했는지(행위)+대상 식별자(id)+결과(건수/상태)정도면 충분한 경우가 많습니다. - 실패 로그:
실패한 행위+입력의 핵심 식별자+예외(스택트레이스)는 거의 고정 세트입니다. - 개인정보/토큰은 기본적으로 마스킹하고, 꼭 필요하면 “길이/해시/마지막 4자리” 같은 형태로 남겨보세요.
핵심 요약: SLF4J로 호출하고 Logback으로 제어하면 로깅을 운영 설정으로 다룰 수 있습니다.
핵심 요약: 레벨 기준을 팀 규칙으로 고정하면 비용과 신뢰도를 동시에 잡을 수 있습니다.
핵심 요약: MDC(requestId)와 예외 스택트레이스는 장애 분석 시간을 크게 줄여줍니다.
다음 글: #34 단위 테스트 시작하기
'JAVA' 카테고리의 다른 글
| Java 성능 체크리스트: String 연결부터 메모리 누수 패턴까지 (0) | 2026.03.01 |
|---|---|
| Java 단위 테스트 시작하기: JUnit 5 기초와 Given-When-Then 패턴 (0) | 2026.03.01 |
| Java Virtual Thread — 경량 스레드의 시대 (Project Loom 실무 가이드) (0) | 2026.02.28 |
| Java switch 패턴 매칭 & 향상된 문법 — switch 표현식부터 가드 패턴까지 (0) | 2026.02.27 |
| Java Record와 Sealed Class로 도메인 모델을 단단하게 만들기 (패턴 매칭까지) (0) | 2026.02.27 |