今回は、JavaやSpring Bootでよく使うSystem.out.printlnを卒業して、Logback/SLF4Jでログを残す方法を新人エンジニア向けに解説します。
新人エンジニアのうちは、動作確認のために次のようなコードを書きがちです。
System.out.println("ここまで来ました");
System.out.println("userId=" + userId);
System.out.println("登録処理が完了しました");学習の最初の段階では、System.out.printlnでも処理の流れを確認できます。
しかし、実務のWebアプリケーションでは、System.out.printlnだけに頼るのはおすすめできません。
なぜなら、ログレベル、出力先、日時、クラス名、エラー詳細、ファイル保存、環境ごとの出し分けなどを管理しにくいからです。
そこで使うのが、SLF4JとLogbackです。
SLF4Jはログ出力の共通窓口、Logbackは実際にログを出力する実行役だと考えると分かりやすいです。SLF4J公式マニュアルでも、SLF4Jはjava.util.logging、Log4j、Logbackなどの各種ロギングフレームワークに対するFacade、つまり抽象化された窓口として説明されています。
System.out.printlnの何が問題なのか
System.out.printlnは、Javaの標準出力に文字を出すための簡単な方法です。
学習中に「この処理が通っているか」を確認するには便利です。
しかし、実務では次のような問題があります。
| 問題 | 説明 |
|---|---|
| 重要度を分けにくい | 情報なのか、警告なのか、エラーなのか分かりにくい |
| 出力を止めにくい | 本番環境で不要な出力が残りやすい |
| ログファイル管理がしにくい | 日付ごとのローテーションなどが難しい |
| クラス名や時刻が見えにくい | どこで出たログか追いにくい |
| 例外の詳細管理が弱い | スタックトレースをきれいに残しにくい |
| 運用で困る | 障害調査時に必要な情報が不足しやすい |
System.out.printlnは、授業中にノートの端へメモを書くようなものです。
その場では役に立ちます。
しかし、チームで共有する業務日誌としては弱いですよね。
実務のログは、あとから調査できる「記録」として残す必要があります。
ログとは何か
ログとは、アプリケーションが動いている間に起きた出来事の記録です。
たとえば、次のような情報を残します。
アプリケーションが起動した ユーザー登録処理が開始された DAOで検索条件を受け取った 外部API呼び出しに失敗した 予期しない例外が発生した
ログは、システムの足跡です。
事件現場で足跡や防犯カメラを見て何が起きたか調べるように、システム障害ではログを見て原因を探します。
ログがないシステムは、暗い部屋で落とし物を探すようなものです。
何が起きたのか分からず、調査に時間がかかります。
SLF4JとLogbackの関係
SLF4JとLogbackの関係を整理しましょう。
| 名前 | 役割 | たとえ |
|---|---|---|
| SLF4J | ログを書くための共通API | 受付窓口 |
| Logback | 実際にログを出力する仕組み | 作業担当者 |
SLF4Jは、アプリケーション側がログを書くための共通の書き方を提供します。
Logbackは、SLF4Jから受け取ったログをコンソールやファイルへ実際に出力します。
レストランでたとえるなら、SLF4Jは注文を受ける店員さんです。
Logbackは、厨房で料理を作って提供する人です。
注文を出す側の私たちは、厨房の細かい仕組みを直接意識せずに、「このログを出してください」とSLF4Jへ依頼します。
Spring Bootでは最初からLogbackが使いやすい
Spring Bootでは、通常のWebアプリケーション開発でspring-boot-starter-webを使うと、ログ出力に必要な仕組みが依存関係として入ります。Spring Boot公式ドキュメントでも、Webアプリケーションではspring-boot-starter-webがspring-boot-starter-loggingに推移的に依存しているため、ログ用の依存関係が追加されると説明されています。
また、Spring BootはデフォルトでLogbackを使う構成になっています。公式ドキュメントでは、スターターを使う場合、デフォルトでLogbackがログに使われると説明されています。
つまり、多くのSpring Bootアプリでは、最初から次のように書けます。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;特別な追加設定をしなくても、まずはコンソールにログを出せます。
新人エンジニアにとっては、ここがうれしいポイントです。
まず使い始めるだけなら難しくありません。
最小のログ出力サンプル
まず、Serviceクラスでログを出す例を見てみましょう。
package com.example.demo.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private static final Logger logger =
LoggerFactory.getLogger(UserService.class);
public void register(String name, String email) {
logger.info("ユーザー登録処理を開始します。name={}, email={}", name, email);
System.out.println("ユーザー登録処理を実行しました");
logger.info("ユーザー登録処理が完了しました。");
}
}ログ出力の中心は次の2行です。
private static final Logger logger =
LoggerFactory.getLogger(UserService.class);
logger.info("ユーザー登録処理を開始します。name={}, email={}", name, email);LoggerFactory.getLogger(UserService.class)で、このクラス専用のLoggerを作っています。
logger.infoで、INFOレベルのログを出しています。
SLF4J公式マニュアルでも、LoggerFactoryからLoggerを取得し、logger.infoでログメッセージを出す基本例が示されています
System.out.printlnからloggerへ書き換える
System.out.printlnを使っているコードを、loggerへ置き換えてみましょう。
置き換え前です。
public void register(String name, String email) {
System.out.println("登録処理を開始します");
System.out.println("name=" + name);
System.out.println("email=" + email);
userDao.insert(name, email);
System.out.println("登録処理が完了しました");
}置き換え後です。
public void register(String name, String email) {
logger.info("登録処理を開始します。name={}, email={}", name, email);
userDao.insert(name, email);
logger.info("登録処理が完了しました。email={}", email);
}loggerでは、文字列連結ではなく、{}を使って値を埋め込めます。
logger.info("登録処理を開始します。name={}, email={}", name, email);
この{}は、プレースホルダーと呼ばれます。
プレースホルダーとは、あとから値を入れるための穴のようなものです。
穴埋め問題をイメージしてください。
名前は ____ です。
この空欄に山田太郎を入れるように、{}へ変数の値が入ります。
なぜ文字列連結より{}を使うのか
次のような書き方は、System.out.printlnに近い感覚です。
logger.debug("userId=" + userId + ", name=" + name);
しかし、ログでは次の書き方のほうがおすすめです。
logger.debug("userId={}, name={}", userId, name);
理由は、ログレベルによって出力されない場合でも、無駄な文字列結合を避けやすいからです。
また、値の位置が分かりやすく、ログメッセージも読みやすくなります。
| 書き方 | 評価 |
|---|---|
| logger.debug("userId=" + userId) | 避けたい |
| logger.debug("userId={}", userId) | おすすめ |
新人エンジニアは、ログでは文字列連結より{}を使う、と覚えてください。
ログレベルを理解する
ログにはレベルがあります。
ログレベルとは、ログの重要度です。
| ログレベル | 意味 | 使う場面 |
|---|---|---|
| trace | 非常に細かい追跡情報 | 細かい内部処理を追いたいとき |
| debug | 開発中の調査情報 | 変数の中身や分岐確認 |
| info | 通常の処理記録 | 処理開始、処理完了、起動情報 |
| warn | 注意が必要な状態 | 処理は継続できるが確認したい異常 |
| error | エラー | 例外や処理失敗 |
たとえるなら、ログレベルは学校の連絡の重要度です。
traceやdebugは、細かいメモです。
infoは、通常の連絡です。
warnは、先生に少し気にしてほしい注意です。
errorは、すぐ確認が必要なトラブルです。
ログレベルの使い分け例
実際のコードで見てみましょう。
logger.trace("検索条件の詳細を確認します。condition={}", condition);
logger.debug("DAOに渡すuserId={}", userId);
logger.info("ユーザー詳細取得を開始します。userId={}", userId);
logger.warn("指定されたユーザーが存在しません。userId={}", userId);
logger.error("ユーザー詳細取得中に例外が発生しました。userId={}", userId, e);ポイントは、何でもinfoにしないことです。
何でもerrorにしないことも大切です。
ログレベルを適切に使うと、運用時に必要な情報を探しやすくなります。
| 悪い使い方 | 理由 |
|---|---|
| 全部info | 重要度が分からない |
| 全部error | 本当に重大なエラーが埋もれる |
| debugに個人情報を大量出力 | 情報漏えいの危険がある |
| warnを何となく使う | 注意すべき状態が分かりにくくなる |
Controllerでログを残す
まず、Controllerでログを使う例です。
package com.example.demo.controller;
import com.example.demo.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class UserController {
private static final Logger logger =
LoggerFactory.getLogger(UserController.class);
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping("/users")
public String register(
@RequestParam String name,
@RequestParam String email) {
logger.info("ユーザー登録リクエストを受け付けました。email={}", email);
userService.register(name, email);
logger.info("ユーザー登録リクエストの処理が完了しました。email={}", email);
return "redirect:/users";
}
}Controllerでは、リクエストを受け取ったこと、処理が終わったことをinfoで残すと分かりやすいです。
ただし、パスワードやクレジットカード番号のような機密情報はログに出してはいけません。
問い合わせフォームやログインフォームでも同じです。
ログはあとから残る情報です。
画面に表示していなくても、ログに出した時点で漏えいリスクがあります。
Serviceでログを残す
Serviceでは、業務処理の開始、完了、重要な分岐を残します。
package com.example.demo.service;
import com.example.demo.model.dao.UserDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private static final Logger logger =
LoggerFactory.getLogger(UserService.class);
private final UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public void register(String name, String email) {
logger.info("ユーザー登録の業務処理を開始します。email={}", email);
boolean exists = userDao.existsByEmail(email);
if (exists) {
logger.warn("既に登録済みのメールアドレスです。email={}", email);
throw new IllegalArgumentException("このメールアドレスは既に登録されています。");
}
userDao.insert(name, email);
logger.info("ユーザー登録の業務処理が完了しました。email={}", email);
}
}Serviceは、業務ルールが集まる場所です。
そのため、「なぜ登録できなかったのか」「どの条件で分岐したのか」がログに残っていると、あとから調査しやすくなります。
ただし、細かすぎるログを出しすぎると、かえって読みにくくなります。
ログは多ければよいわけではありません。
必要な情報を、必要なレベルで残すことが大切です。
DAOでログを残す
DAOでは、SQLそのものよりも、検索条件や処理結果を分かる範囲で残すと便利です。
package com.example.demo.model.dao;
import com.example.demo.model.dto.UserDto;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
@Repository
public class UserDao extends SuperDao {
private static final Logger logger =
LoggerFactory.getLogger(UserDao.class);
public Optional<UserDto> findById(long userId) {
logger.debug("ユーザーIDによる検索を開始します。userId={}", userId);
String sql =
"SELECT user_id, name, email "
+ "FROM users "
+ "WHERE user_id = ?";
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
UserDto user = new UserDto();
user.setUserId(rs.getLong("user_id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
logger.debug("ユーザーIDによる検索に成功しました。userId={}", userId);
return Optional.of(user);
}
logger.debug("ユーザーIDによる検索結果は0件でした。userId={}", userId);
return Optional.empty();
}
} catch (SQLException e) {
logger.error("ユーザーIDによる検索中にSQL例外が発生しました。userId={}", userId, e);
throw new RuntimeException("ユーザー検索に失敗しました。", e);
}
}
}DAOではdebugを使うことが多いです。
なぜなら、通常運用ではDAOの細かい処理までは常に見なくてもよいからです。
不具合調査のときだけdebugレベルを有効にすると、検索条件や処理の流れを追いやすくなります。
例外ログでは例外オブジェクトを最後に渡す
例外をログに残すときは、例外オブジェクトを最後の引数に渡します。
良い例です。
logger.error("ユーザー検索中に例外が発生しました。userId={}", userId, e);
悪い例です。
logger.error("ユーザー検索中に例外が発生しました。" + e.getMessage());
良い例では、スタックトレースがログに出ます。
スタックトレースとは、例外がどのメソッドを通って発生したのかを示す情報です。
エラー調査では、スタックトレースが非常に重要です。
たとえるなら、スタックトレースは事件現場までの足跡です。
足跡があれば、どこから問題が始まったのか追跡できます。
@ControllerAdviceで例外ログを共通化する
Spring Bootでは、@ControllerAdviceで例外処理を共通化できます。
例外ログもここに集めると、Controllerごとのtry-catchを減らせます。
package com.example.demo.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger =
LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(IllegalArgumentException.class)
public String handleIllegalArgumentException(
IllegalArgumentException e,
Model model) {
logger.warn("不正なリクエストです。message={}", e.getMessage());
model.addAttribute("errorMessage", e.getMessage());
return "error/400";
}
@ExceptionHandler(Exception.class)
public String handleException(
Exception e,
Model model) {
logger.error("予期しないエラーが発生しました。", e);
model.addAttribute("errorMessage", "予期しないエラーが発生しました。");
return "error/500";
}
}ここでは、想定できる入力不正をwarnにしています。
予期しない例外はerrorにしています。
重要なのは、ユーザーに見せるメッセージと、開発者が見るログを分けることです。
ユーザーには「予期しないエラーが発生しました」と表示します。
開発者向けのログには、例外の詳細を残します。
ログレベルをapplication.propertiesで変更する
Spring Bootでは、application.propertiesでログレベルを変更できます。
logging.level.root=INFO
logging.level.com.example.demo=DEBUG
logging.level.org.springframework.web=INFOこの設定の意味です。
| 設定 | 意味 |
|---|---|
| logging.level.root=INFO | 全体の基本ログレベルをINFOにする |
| logging.level.com.example.demo=DEBUG | 自分のアプリのログをDEBUGまで出す |
| logging.level.org.springframework.web=INFO | Spring Web関連のログをINFOにする |
Spring Boot公式ドキュメントでも、logging.levelというプレフィックスを使ってロガーごとのログレベルをapplication.propertiesで設定できると説明されています。
開発中はDEBUGを出したい。
本番ではINFO以上にしたい。
このように、環境によってログの細かさを変えるのが一般的です。
ログをファイルに出す
コンソールだけでなく、ログファイルにも出したい場合があります。
簡単な設定なら、application.propertiesで次のように書けます。
logging.file.name=logs/app.logこの設定により、logs/app.logへログを出力できます。
Spring Boot公式ドキュメントでも、logging.file.nameを使ってコンソールに加えてログを書き込むファイルの場所を設定できると説明されています。
ただし、実務ではログファイルが無限に大きくならないように、ローテーションを設定することが多いです。
ローテーションとは、ログファイルを日付やサイズで分割する仕組みです。
ノート1冊にすべての記録を書き続けると、いつか読みにくくなりますよね。
日ごとにノートを分けるように、ログも日付やサイズで分けます。
logback-spring.xmlでログ設定を書く
より細かくログを設定したい場合は、logback-spring.xmlを使います。
ファイルの場所です。
src/main/resources/logback-spring.xml
基本例です。
<configuration>
<property name="LOG_DIR" value="logs" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.example.demo" level="DEBUG" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>Spring Boot公式ドキュメントでは、application.propertiesだけでは足りないLogbackの細かい設定を行う場合、クラスパスのルートにlogback.xmlを置けること、Spring Bootの拡張を使いたい場合はlogback-spring.xmlを使えることが説明されています。
logback-spring.xmlの中身を理解する
設定の意味を分解します。
| 要素 | 意味 |
|---|---|
| configuration | Logback設定全体 |
| property | 設定内で使う変数 |
| appender | ログの出力先 |
| encoder | ログの書式 |
| rollingPolicy | ログファイルの分割ルール |
| logger | 特定パッケージのログレベル |
| root | 全体の基本ログ設定 |
Appenderは、ログの出力先を表します。
コンソールへ出すならConsoleAppender。
ファイルへ出すならFileAppenderやRollingFileAppenderを使います。
Logbackの公式マニュアルでも、Appenderはログイベントを適切な出力先へ出力する役目を持つ部品として説明されています。
ログの出力形式を読む
次のpatternを見てみましょう。
%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n
これは、ログ1行の表示形式です。
| 記号 | 意味 |
|---|---|
| %d | 日時 |
| %-5level | ログレベル |
| %thread | スレッド名 |
| %logger | Logger名、つまり主にクラス名 |
| %msg | ログメッセージ |
| %n | 改行 |
出力例です。
2026-06-18 10:15:30 INFO [http-nio-8080-exec-1] c.e.demo.controller.UserController - ユーザー登録リクエストを受け付けました。email=yamada@example.com
この1行から、いつ、どのレベルで、どのスレッドで、どのクラスから、どんなメッセージが出たか分かります。
System.out.printlnよりも、はるかに調査しやすいですね。
開発環境と本番環境でログレベルを変える
開発環境では、細かいdebugログが欲しいことがあります。
本番環境では、debugを出しすぎるとログが膨大になり、個人情報や内部情報の漏えいリスクも上がります。
そのため、環境ごとにログレベルを変えるのが基本です。
application-dev.propertiesです。
logging.level.com.example.demo=DEBUGapplication-prod.propertiesです。
logging.level.com.example.demo=INFO開発環境では詳しく。
本番環境では必要な情報に絞る。
この切り替えができるのも、ログフレームワークを使う大きなメリットです。
やってはいけないログ出力
ログは便利ですが、何でも出してよいわけではありません。
特に、次の情報はログに出さないようにしてください。
| 出してはいけない情報 | 理由 |
|---|---|
| パスワード | 漏えい時の被害が大きい |
| パスワードハッシュ | 解析対象になる可能性がある |
| クレジットカード番号 | 重大な情報漏えいになる |
| APIキー | 不正利用される可能性がある |
| セッションID | なりすましにつながる可能性がある |
| 個人情報の大量出力 | 運用ログから漏えいする可能性がある |
悪い例です。
logger.info("ログイン処理。email={}, password={}", email, password);
良い例です。
logger.info("ログインリクエストを受け付けました。email={}", email);
パスワードは絶対に出さないでください。
ログは開発者だけでなく、運用担当、監視システム、ログ収集基盤などにも渡ることがあります。
「画面に出していないから安全」ではありません。
ログに出した時点で、情報は残ります。
ログメッセージの書き方のコツ
良いログメッセージには、いくつかの特徴があります。
| 良いログ | 理由 |
|---|---|
| 何の処理か分かる | あとから検索しやすい |
| 重要なIDが入っている | 対象データを追いやすい |
| 開始と完了が分かる | どこで止まったか分かりやすい |
| 例外時にスタックトレースがある | 原因調査しやすい |
| 秘密情報を含まない | 安全に運用できる |
悪いログです。
logger.info("処理開始");
logger.info("完了");
logger.error("エラー");何の処理か分かりません。
良いログです。
logger.info("ユーザー登録処理を開始します。email={}", email);
logger.info("ユーザー登録処理が完了しました。userId={}", userId);
logger.error("ユーザー登録処理中に例外が発生しました。email={}", email, e);処理名と重要な識別子が入っているため、あとから追いやすくなります。
ログを入れすぎるデメリット
ログは大切ですが、入れすぎると逆に困ります。
| デメリット | 説明 |
|---|---|
| ログが読みにくくなる | 重要な情報が埋もれる |
| ディスク容量を使う | ファイルが巨大化する |
| 性能に影響する | 大量出力で処理が重くなる可能性がある |
| 情報漏えいリスクが増える | 不要なデータまで残る |
ログは、調味料に似ています。
少なすぎると味が分かりません。
多すぎると料理が台無しになります。
必要な場所に、必要な量だけ入れることが大切です。
System.out.printlnを残してよい場面
では、System.out.printlnは完全に禁止なのでしょうか。
学習中の小さなmainメソッドや、数分で消す一時的な確認なら使っても構いません。
ただし、Spring BootのController、Service、DAOに残すのは避けましょう。
| 場面 | System.out.println | logger |
|---|---|---|
| Javaの入門学習 | 使ってよい | まだ不要なこともある |
| 一時的な動作確認 | 使う場合は後で消す | できればlogger |
| Spring Bootの実装 | 避ける | 使う |
| チーム開発 | 避ける | 使う |
| 本番運用 | 避ける | 必須 |
新人エンジニアは、Spring Bootに入ったらSystem.out.printlnからloggerへ移行する、と覚えてください。
Lombokの@Slf4jを使う方法
プロジェクトでLombokを使っている場合は、@Slf4jでLogger定義を省略できます。
package com.example.demo.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class UserService {
public void register(String email) {
log.info("ユーザー登録処理を開始します。email={}", email);
log.info("ユーザー登録処理が完了しました。email={}", email);
}
}@Slf4jを使うと、次のようなLogger定義を書かなくて済みます。
private static final Logger logger =
LoggerFactory.getLogger(UserService.class);
ただし、新人エンジニアは最初にLoggerFactoryを使う基本形を理解してから、@Slf4jを使うほうがおすすめです。
省略記法から入ると、裏側で何が起きているか分かりにくくなるためです。
新人エンジニア向けのログ導入手順
既存コードにSystem.out.printlnがある場合、次の順番で置き換えるとよいです。
1. クラスにLoggerを定義する 2. System.out.printlnをlogger.infoまたはlogger.debugへ置き換える 3. 例外箇所ではlogger.errorを使う 4. パスワードや秘密情報を出していないか確認する 5. application.propertiesでログレベルを調整する 6. 必要ならlogback-spring.xmlでファイル出力を設定する
一気に全クラスを書き換えようとすると大変です。
まずはControllerから。
次にService。
最後にDAO。
このように、範囲を決めて少しずつ置き換えると安全です。
よくあるミス
| ミス | 問題 | 改善 |
|---|---|---|
| Loggerをstatic finalにしていない | 毎回作る必要が出る | private static final Loggerにする |
| 文字列連結でログを書く | 読みにくく無駄が出やすい | {}を使う |
| 例外をe.getMessageだけ出す | 原因追跡が難しい | 例外オブジェクトを最後に渡す |
| パスワードをログに出す | 重大な情報漏えい | 絶対に出さない |
| debugログが本番で大量に出る | ログ肥大化や漏えいリスク | 本番ではINFO以上にする |
| ログメッセージが短すぎる | 何の処理か分からない | 処理名とIDを含める |
完成形のサンプル
最後に、Controller、Service、DAO、例外処理までログを入れた簡単な流れをまとめます。
Controllerです。
package com.example.demo.controller;
import com.example.demo.model.dto.UserDto;
import com.example.demo.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class UserController {
private static final Logger logger =
LoggerFactory.getLogger(UserController.class);
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/users/detail")
public String detail(@RequestParam long userId, Model model) {
logger.info("ユーザー詳細画面のリクエストを受け付けました。userId={}", userId);
UserDto user = userService.findById(userId);
model.addAttribute("user", user);
logger.info("ユーザー詳細画面の表示準備が完了しました。userId={}", userId);
return "users/detail";
}
}Serviceです。
package com.example.demo.service;
import com.example.demo.exception.UserNotFoundException;
import com.example.demo.model.dao.UserDao;
import com.example.demo.model.dto.UserDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private static final Logger logger =
LoggerFactory.getLogger(UserService.class);
private final UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public UserDto findById(long userId) {
logger.debug("ユーザー詳細取得処理を開始します。userId={}", userId);
UserDto user = userDao.findById(userId)
.orElseThrow(() -> {
logger.warn("指定されたユーザーが見つかりません。userId={}", userId);
return new UserNotFoundException("ユーザーが見つかりません。userId=" + userId);
});
logger.debug("ユーザー詳細取得処理が完了しました。userId={}", userId);
return user;
}
}DAOです。
package com.example.demo.model.dao;
import com.example.demo.model.dto.UserDto;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
@Repository
public class UserDao extends SuperDao {
private static final Logger logger =
LoggerFactory.getLogger(UserDao.class);
public Optional<UserDto> findById(long userId) {
logger.debug("ユーザー検索SQLを実行します。userId={}", userId);
String sql =
"SELECT user_id, name, email "
+ "FROM users "
+ "WHERE user_id = ?";
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
UserDto user = new UserDto();
user.setUserId(rs.getLong("user_id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
logger.debug("ユーザー検索SQLの結果が見つかりました。userId={}", userId);
return Optional.of(user);
}
logger.debug("ユーザー検索SQLの結果は0件でした。userId={}", userId);
return Optional.empty();
}
} catch (SQLException e) {
logger.error("ユーザー検索SQLで例外が発生しました。userId={}", userId, e);
throw new RuntimeException("ユーザー検索に失敗しました。", e);
}
}
}@ControllerAdviceです。
package com.example.demo.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger =
LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(UserNotFoundException.class)
public String handleUserNotFoundException(
UserNotFoundException e,
Model model) {
logger.warn("ユーザー未存在エラーを処理しました。message={}", e.getMessage());
model.addAttribute("errorMessage", e.getMessage());
return "error/404";
}
@ExceptionHandler(Exception.class)
public String handleException(
Exception e,
Model model) {
logger.error("未処理例外を処理しました。", e);
model.addAttribute("errorMessage", "予期しないエラーが発生しました。");
return "error/500";
}
}まとめ
System.out.printlnは、学習の初期や一時的な確認には便利です。
しかし、Spring Bootでチーム開発や実務開発をするなら、SLF4JとLogbackを使ってログを残すべきです。
| 用語 | 意味 |
|---|---|
| System.out.println | 標準出力に文字を出す簡易的な方法 |
| SLF4J | ログ出力の共通窓口 |
| Logback | ログを実際にコンソールやファイルへ出す仕組み |
| Logger | ログを出すためのオブジェクト |
| ログレベル | trace、debug、info、warn、errorなどの重要度 |
| Appender | ログの出力先 |
| logback-spring.xml | Logbackの詳細設定を書くファイル |
一言でまとめるなら、System.out.printlnは「その場の確認メモ」、Logback/SLF4Jのログは「あとから調査できる業務記録」です。
新人エンジニアは、まず次の形を覚えてください。
private static final Logger logger =
LoggerFactory.getLogger(クラス名.class);
logger.info("処理を開始します。id={}", id);
logger.error("例外が発生しました。id={}", id, e);そして、次のルールを守りましょう。
- System.out.printlnをController、Service、DAOに残さない
- ログレベルを使い分ける
- 文字列連結ではなく{}を使う
- 例外ログでは例外オブジェクトを最後に渡す
- パスワードやAPIキーをログに出さない
- 本番ではdebugログを出しすぎない
今後の学習では、SLF4J、Logback、ログレベル、application.propertiesのlogging.level、logback-spring.xml、RollingFileAppender、@ControllerAdviceでの例外ログ、ログに出してよい情報と出してはいけない情報を順番に学ぶとよいです。まずは既存コードのSystem.out.printlnを探し、logger.info、logger.debug、logger.errorへ置き換えるところから始めてみましょう!