こんにちは、かつコーチです。
「本番環境で不具合が起きたのに、System.out.println()をどこにも仕込んでいなくて原因が追えない」という経験はありませんか。
今回は、Javaの標準ログ機能java.util.loggingと、実務で広く使われるSLF4Jの基本を整理します。
なぜprintlnではなくログライブラリを使うのか
System.out.printlnの限界
System.out.println()は手軽ですが、実務では次のような問題があります。
- 出力の重要度(エラーか、情報程度か)を区別できない
- 本番環境で出力を止める・絞ることが簡単にできない
- 出力先をファイルに切り替えるといった柔軟性がない
- タイムスタンプやクラス名などの付加情報が自動で付かない
これらの問題を解決するのがロギングフレームワーク(ログの出力先・形式・重要度を柔軟に管理する仕組み)です。
java.util.logging:Java標準のログAPI
基本の使い方
java.util.logging(通称JUL)はJavaに標準搭載されているため、追加のライブラリなしで使えます。
import java.util.logging.Logger;
import java.util.logging.Level;
public class OrderService {
private static final Logger logger = Logger.getLogger(OrderService.class.getName());
public void processOrder(int orderId) {
logger.info("注文処理を開始: orderId=" + orderId);
try {
// 処理
} catch (Exception e) {
logger.log(Level.SEVERE, "注文処理でエラー発生: orderId=" + orderId, e);
}
}
}
実行すると、次のように時刻・クラス名・メソッド名付きで出力されます。
2026-08-31 10:15:22 情報: com.example.OrderService processOrder
注文処理を開始: orderId=1001
ログレベルの使い分け
JULには重要度に応じたレベルが用意されており、状況に応じて使い分けます。
| レベル | 用途 |
|---|---|
SEVERE | 処理を継続できない重大なエラー |
WARNING | 想定外だが処理は継続できる状況 |
INFO | 通常の処理の節目を示す情報 |
FINE〜FINEST | 詳細なデバッグ用の情報 |
SLF4Jで実務標準のログを扱う
なぜSLF4Jが実務で好まれるのか
SLF4J(Simple Logging Facade for Java)は、Logback・Log4j2など複数のログ実装を統一的なAPIで扱えるファサード(実装の違いを隠して統一的な窓口を提供する仕組み)です。
実務のJavaプロジェクトの多くは、JULではなくSLF4J+Logbackの組み合わせを採用しています。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
public void processOrder(int orderId) {
logger.info("注文処理を開始: orderId={}", orderId);
}
}
SLF4Jの大きな特徴が{}によるプレースホルダー記法です。
// ❌Before:文字列連結でログメッセージを組み立てる
logger.info("注文処理を開始: orderId=" + orderId + ", userId=" + userId);
// ✅After:プレースホルダーで組み立てる
logger.info("注文処理を開始: orderId={}, userId={}", orderId, userId);
文字列連結は、ログレベルの設定によって出力されない場合でも連結処理自体は毎回実行されてしまいます。
プレースホルダー方式なら、実際にログが出力される場合にのみ文字列の組み立てが行われるため、わずかながら性能面でも有利です。
つまずいたポイント:例外を握りつぶすログの書き方
実務でレビューを担当した際、次のようなログの書き方が原因で本番障害の調査が難航したケースがありました。
// ❌Before:例外オブジェクトをログに渡していない
try {
orderRepository.save(order);
} catch (Exception e) {
logger.error("保存に失敗しました: " + e.getMessage());
}
このコードで実際に出力されたログは次の1行だけでした。
2026-08-31 14:02:10 ERROR OrderService - 保存に失敗しました: Duplicate entry '1001' for key 'PRIMARY'
e.getMessage()だけでは、スタックトレース(例外がどのメソッドを経由して発生したかの呼び出し履歴)が失われてしまい、どのコードから呼ばれた保存処理で発生したのかまでは分かりませんでした。
// ✅After:例外オブジェクトそのものをログに渡す
try {
orderRepository.save(order);
} catch (Exception e) {
logger.error("保存に失敗しました: orderId={}", order.getId(), e);
}
こうすることで、SLF4Jが自動的にスタックトレース全体をログに出力してくれます。
2026-08-31 14:02:10 ERROR OrderService - 保存に失敗しました: orderId=1001
org.springframework.dao.DuplicateKeyException: Duplicate entry '1001' for key 'PRIMARY'
at com.example.OrderRepository.save(OrderRepository.java:34)
at com.example.OrderService.processOrder(OrderService.java:18)
「例外を文字列に変換してからログに書く」のではなく、「例外オブジェクトをそのまま最後の引数として渡す」ことを徹底するだけで、障害調査のスピードが大きく変わります。
まとめ
この記事のポイント
System.out.println()は重要度の区別や出力先の柔軟な切り替えができないjava.util.loggingはJava標準で追加ライブラリ不要、SEVERE〜FINESTのレベルを使い分ける- 実務では複数実装を統一的に扱える
SLF4J(Logback等と組み合わせ)が広く使われる - ログメッセージは文字列連結ではなく
{}プレースホルダーで組み立てる - 例外を記録する際は
e.getMessage()だけでなく例外オブジェクト自体を渡し、スタックトレースを残す
次に読むべき記事
- スタックトレースの詳しい読み方は「よくあるコンパイルエラーの読み方」の記事へ
- 例外発生時の原因調査は「デバッグの基本(IDEのブレークポイント)」の記事へ
タグ: Java, 中級者向け, エラー解決