SJ blog
Java
Z

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

Javaのログ出力ベストプラクティス — SLF4J + Logback 実践ガイド

System.out.println をやめてSLF4J+Logbackに移行する方法、ログレベルの使い分け、MDCを使ったリクエストID付与、ログローテーション設定を解説します。

一言結論

本番コードにSystem.out.printlnを使うのは論外で、SLF4J+Logbackを使いMDCでリクエストIDを付与することで分散環境でも1リクエストのログを追跡可能にするのが現代の標準だ。

なぜ System.out.println を使ってはいけないか

問題説明
レベルがないエラーも情報も同じ扱い
無効化できない本番でも全部出力される
フォーマットが貧弱タイムスタンプ、スレッド名などがない
パフォーマンス同期処理でスループットが下がる

SLF4J + Logback の導入

<!-- pom.xml -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.11</version>
</dependency>
<!-- SLF4J API は logback-classic が自動で含む -->

Spring Boot を使っている場合は spring-boot-starter-logging が自動で含まれます。

基本的な使い方

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class UserService {
    // クラスごとにロガーを定義(static final 推奨)
    private static final Logger log = LoggerFactory.getLogger(UserService.class);
    
    public User findUser(int id) {
        log.debug("findUser called: id={}", id);
        
        User user = repository.find(id);
        if (user == null) {
            log.warn("User not found: id={}", id);
            return null;
        }
        
        log.info("User found: id={}, name={}", id, user.getName());
        return user;
    }
    
    public void deleteUser(int id) {
        try {
            repository.delete(id);
            log.info("User deleted: id={}", id);
        } catch (Exception e) {
            log.error("Failed to delete user: id={}", id, e);  // 例外はこう渡す
            throw e;
        }
    }
}

重要: log.error("message" + e.getMessage()) ではなく log.error("message", e) でスタックトレースが出力されます。

ログレベルの使い分け

レベル使い場面
TRACE非常に詳細な処理追跡(開発時のみ)
DEBUGデバッグに必要な情報(テスト環境向け)
INFO正常な動作の記録(起動、主要な処理完了)
WARN問題ではないが注意が必要な事象
ERRORエラー(処理は続行可能)

{} プレースホルダーを使う理由

// NG: 文字列結合(ログが無効でも結合が実行される)
log.debug("User: " + user.toString());

// OK: プレースホルダー(ログが無効なら toString() も実行されない)
log.debug("User: {}", user);

// NG: isDebugEnabled() チェックは通常不要
if (log.isDebugEnabled()) {
    log.debug("User: {}", user.toString());  // {} を使えばこれも不要
}

Logback の設定(logback.xml)

src/main/resources/logback.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- コンソール出力 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</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.gz</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <maxHistory>30</maxHistory>  <!-- 30日分保持 -->
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{requestId}] - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- パッケージごとのレベル設定 -->
    <logger name="com.example" level="DEBUG"/>
    <logger name="org.springframework" level="WARN"/>
    <logger name="org.hibernate.SQL" level="DEBUG"/>

    <!-- ルートロガー -->
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>

</configuration>

MDC(Mapped Diagnostic Context)でリクエストIDを追跡

import org.slf4j.MDC;

// サーブレットフィルターやインターセプターで設定
public class RequestIdFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        String requestId = UUID.randomUUID().toString().substring(0, 8);
        MDC.put("requestId", requestId);
        try {
            chain.doFilter(req, res);
        } finally {
            MDC.clear();  // スレッドプールなので必ずクリア
        }
    }
}

logback.xml のパターンで %X{requestId} として出力します。これで同一リクエストのログが追跡しやすくなります。

2026-04-06 10:30:00.123 [http-nio-8080-exec-1] INFO  UserService [a1b2c3d4] - User found: id=42
2026-04-06 10:30:00.456 [http-nio-8080-exec-1] INFO  OrderService [a1b2c3d4] - Order created: 99

環境ごとのログ設定

Spring Boot の場合 application.properties で簡単に設定:

# 開発環境
logging.level.root=INFO
logging.level.com.example=DEBUG
logging.file.name=logs/app.log
logging.logback.rollingpolicy.max-file-size=100MB
logging.logback.rollingpolicy.max-history=30

Lombok の @Slf4j アノテーション

// Lombok を使うと1行で済む
@Slf4j
public class UserService {
    public void process() {
        log.info("Processing...");  // log は自動生成
    }
}

まとめ

  1. System.out.println を SLF4J + Logback に移行する
  2. {} プレースホルダーを使って文字列結合を避ける
  3. 例外は log.error("msg", e) で渡す(スタックトレースが出る)
  4. MDC でリクエスト ID を付与して追跡可能にする
  5. ログローテーションでディスクを圧迫しない設定にする