今回は、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=INFOSpring 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の中身を理解する

設定の意味を分解します。

要素意味
configurationLogback設定全体
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スレッド名
%loggerLogger名、つまり主にクラス名
%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=DEBUG




application-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.printlnlogger
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.xmlLogbackの詳細設定を書くファイル

一言でまとめるなら、System.out.printlnは「その場の確認メモ」、Logback/SLF4Jのログは「あとから調査できる業務記録」です。

新人エンジニアは、まず次の形を覚えてください。

private static final Logger logger =
        LoggerFactory.getLogger(クラス名.class);

logger.info("処理を開始します。id={}", id);

logger.error("例外が発生しました。id={}", id, e);




そして、次のルールを守りましょう。

  1. System.out.printlnをController、Service、DAOに残さない
  2. ログレベルを使い分ける
  3. 文字列連結ではなく{}を使う
  4. 例外ログでは例外オブジェクトを最後に渡す
  5. パスワードやAPIキーをログに出さない
  6. 本番ではdebugログを出しすぎない

今後の学習では、SLF4J、Logback、ログレベル、application.propertiesのlogging.level、logback-spring.xml、RollingFileAppender、@ControllerAdviceでの例外ログ、ログに出してよい情報と出してはいけない情報を順番に学ぶとよいです。まずは既存コードのSystem.out.printlnを探し、logger.info、logger.debug、logger.errorへ置き換えるところから始めてみましょう!

最後までお読みいただきありがとうございます。