Spring 学習ログ

以下は学習した内容を AI に整理してもらいました。

Kotlin + Spring Boot で「最小の REST API / Logging / AOP」を触ってみた記録

はじめに

Spring に少し興味があり、とはいえあまり時間もかけれない状況のため4時間以内で終わる最小構成を目標に学習した。
今回の目的は、深く極めることではなく、少し手を動かして、何をやったか・どう動いているかを説明できる状態になること

前提として、Kotlin は少し触ったことがあり、Laravel の Controller / Service / Repository に近い考え方もなんとなく分かる。
ただ、Spring はまだ弱く、専用用語も曖昧だったので、なるべく丁寧に意味を確認しながら進めた。


今回のゴール

今回、最低限できるようになりたかったのは次の内容。

  • Spring Boot アプリを作って起動できる
  • REST API を 1〜2本動かせる
  • Controller / Service / Repository の役割を説明できる
  • Logging を追加して、ログが出るのを確認できる
  • AOP を追加して、メソッドの前後で共通処理が動くのを確認できる

先に結論

今回の学習では、最終的に以下を確認できた。

  • GET /hello が動く
  • GET /todos が動く
  • TodoControllerTodoServiceTodoRepository の流れが動く
  • TodoServicelogger.debug() / logger.info() を書いてログを確認できた
  • application.properties でログレベルを DEBUG に設定できた
  • LoggingAspect を作り、TodoService.getTodos() の前後で AOP ログを出せた

つまり、Spring Boot の最小構成 + Logging + AOP まで体験できた


なぜ CLI ではなく REST API にしたのか

最初は、CLI だけで動くものを作るか、REST API にするかで迷った。
結論としては、今回は REST API の方が理解しやすかった

理由は次のとおり。

  • Spring の強みである「HTTP を受ける」「Controller が入口になる」が見えやすい
  • Controller / Service / Repository の役割分担を説明しやすい
  • ブラウザで http://localhost:8080/... を叩いて結果が見える
  • Logging や AOP の効果を、リクエストごとに確認しやすい

HTML の画面は今回は作らなかった。
理由は、画面まで入れると学習対象が増えすぎて、Spring(2) の達成よりもフロント側の理解に時間を使ってしまうから。


環境と方針

  • エディタ / IDE: IntelliJ IDEA
  • 言語: Kotlin
  • フレームワーク: Spring Boot
  • ビルドツール: Gradle
  • 設定ファイル形式: properties
  • DB: 使わない
  • データ保存: メモリ上の固定値
  • 目的: スキルマップ達成を優先して最小構成で進める

まず整理した用語

Kotlin と Spring の違い

最初に混乱したのがここだった。

  • Kotlin はプログラミング言語
  • Spring はアプリを組み立てるためのフレームワーク
  • Spring Boot は Spring を始めやすくした便利セット

つまり、

  • Kotlin = 何語で書くか
  • Spring = どういう仕組みで組み立てるか

という違い。


REST API とは

HTTP でやりとりする API のこと。
今回だと、例えば次のようなもの。

  • GET /hello
  • GET /todos

URL にアクセスすると、Spring が処理して結果を返す。
今回は HTML ではなく、文字列や JSON 配列が返るのを確認した。


Logging とは

アプリの動作記録を残すこと。
たとえば、

  • どの処理が呼ばれたか
  • いつ呼ばれたか
  • エラーが起きたか

をコンソールやファイルに出す。

今回は println ではなく logger.info() / logger.debug() を使った。
ログレベルを設定できるのが重要。


AOP とは

AOP は Aspect-Oriented Programming の略。
最初は難しく感じたけれど、今回は次の理解で十分だった。

「共通の処理を、本体の処理の外から差し込む仕組み」

今回でいうと、

  • TodoService.getTodos() の前にログ
  • TodoService.getTodos() の後にログ

を、Service 本体に直接書かず、別クラスから差し込んだ。

Laravel のミドルウェアに少し似ているけれど、違いもある。

  • Laravel のミドルウェア
    → HTTP リクエストの入口全体に効く感じ
  • Spring の AOP
    → メソッド単位で前後に効かせられる

今回は TodoService.getTodos() という Service のメソッド に対して AOP を使った。


Controller / Service / Repository の役割

今回、自分なりに整理できたのは次の役割分担。

  • Controller
    HTTP リクエストの入口。URL を受けて、Service を呼び、結果を返す。
  • Service
    処理の中身を書く場所。業務ロジックの中心。
  • Repository
    データ取得・保存担当。今回は DB は使わず、固定データを返すだけの役割にした。

DI とは

DI は Dependency Injection の略。
最初の理解としては「必要なクラスを自分で new せず、Spring から受け取る仕組み」で十分だった。

少しエンジニア寄りに言うと、

依存オブジェクトの生成責務を利用側から分離して、Spring コンテナに任せる仕組み

という理解になった。

例えば TodoController は自分で TodoService() を作らず、コンストラクタで受け取る。

class TodoController(
    private val todoService: TodoService
)

これにより、

  • 依存関係が明示的になる
  • 生成責務を持たなくてよい
  • テストや差し替えがしやすい

というメリットがある。


Bean とは

Spring コンテナに管理されているオブジェクトのこと。
今回は、

  • @RestController
  • @Service
  • @Repository
  • @Component

がついたクラスを Spring が見つけて管理する、という感覚で理解した。


プロジェクト作成時に気になったこと

Java / Kotlin / Groovy と Spring Boot の違い

IntelliJ の新規プロジェクト作成画面で最初に迷った。

  • Java / Kotlin / Groovy
    → 何語で書くか
  • Spring Boot
    → どういう種類のアプリを作るか

今回は Kotlin という言語で、Spring Boot プロジェクトを作る、という組み合わせになる。


JDK と Java バージョンの違い

ここも混乱した。

  • JDK
    実際の開発実行環境
  • Java 17
    プロジェクト側が要求する対象バージョン

今回ハマったのは、プロジェクトは Java 17 を必要としているのに、手元で Gradle から見える Java 17 がなかったこと。

エラーはこうだった。

  • Cannot find a Java installation ... matching 17

最終的には、Mac 全体に Java を入れず、プロジェクト配下にローカル JDK を置いて解決した。


Jar と War の違い

今回は Jar を選んだ。

  • Jar
    単体で起動しやすい。Spring Boot でよく使う。
  • War
    外部のアプリサーバに載せる前提が強い形式。

学習用としては Jar が自然だった。


properties と yaml の違い

どちらも設定ファイルの書き方。

  • properties
    1行ずつ key=value で書く。最初は分かりやすい。
  • yaml
    階層構造が見やすい。インデントに注意。

今回は最初なので application.properties を使った。


Gradle とは

Gradle はビルドツール。
依存ライブラリを集めたり、コンパイルしたり、実行したりするための仕組み。

build.gradle.kts は、その設定ファイル。
名前の意味はざっくりこう。

  • build = ビルド設定
  • gradle = Gradle 用
  • kts = Kotlin Script で書かれている

実際に作ったもの

1. 起動クラス

最初の起点は main 関数だった。

@SpringBootApplication
class SpringTodoApplication

fun main(args: Array<String>) {
    runApplication<SpringTodoApplication>(*args)
}

ここで runApplication を呼ぶことで、Spring Boot アプリが起動する。


2. HelloController

まずは最小の疎通確認として GET /hello を作った。

@RestController
class HelloController {

    @GetMapping("/hello")
    fun hello(): String {
        return "hello"
    }
}

これで http://localhost:8080/hello にアクセスすると hello が返る。


3. TodoController

次に GET /todos を作った。

最初は Controller に直接書いた。

@RestController
class TodoController {

    @GetMapping("/todos")
    fun getTodos(): List<String> {
        return listOf("Springを触る", "AOPを学ぶ")
    }
}

これで JSON 配列が返るのを確認した。

["Springを触る","AOPを学ぶ"]

4. TodoService に分離

次に、Controller の中身を Service に移した。

@Service
class TodoService {

    fun getTodos(): List<String> {
        return listOf("Springを触る", "AOPを学ぶ")
    }
}
@RestController
class TodoController(
    private val todoService: TodoService
) {

    @GetMapping("/todos")
    fun getTodos(): List<String> {
        return todoService.getTodos()
    }
}

ここで、Controller は入口、Service は処理本体、という役割分担が見えた。


5. TodoRepository を追加

さらに Repository を追加し、Service から呼ぶようにした。

@Repository
class TodoRepository {

    fun findAll(): List<String> {
        return listOf("Springを触る", "AOPを学ぶ")
    }
}
@Service
class TodoService(
    private val todoRepository: TodoRepository
) {

    fun getTodos(): List<String> {
        return todoRepository.findAll()
    }
}

これで処理の流れはこうなった。

  • リクエスト
  • Controller
  • Service
  • Repository
  • 結果を返す

起動して確認したこと

アプリを起動すると、ログに次のような内容が出た。

  • Spring Boot 3.5.13 で起動している
  • Java 17.0.18 を使っている
  • Tomcat が 8080 番ポートで立ち上がっている

特に大事だと理解したログはこれ。

  • Tomcat started on port 8080 (http) with context path '/'
  • Started SpringTodoApplicationKt ...

これはつまり、

「ローカルの Web サーバとして Spring Boot が起動し、HTTP を受けられる状態になった」

という意味だった。


/ にアクセスしたら Whitelabel Error Page が出た件

起動後に http://localhost:8080 へアクセスしたら、最初は Whitelabel Error Page が出た。

これは壊れているわけではなく、単に / に対応する処理をまだ作っていないため。

つまり、

  • アプリは起動している
  • リクエストは届いている
  • ただし / の mapping がない

という状態だった。

ここで、GET /hello のように URL と処理の対応づけが必要だと分かった。


Logging を追加して確認したこと

TodoService にロガーを追加した。

private val logger = LoggerFactory.getLogger(TodoService::class.java)

fun getTodos(): List<String> {
    logger.info("getTodos called")
    return todoRepository.findAll()
}

/todos を叩くと、次のようなログが出た。

  • com.example.springtodo.TodoService : getTodos called

これで、

Service の処理が呼ばれたことをログで確認できた

という状態になった。


DispatcherServlet って何だったか

ログに出てきた DispatcherServlet も気になった。

最初の理解としては、

Spring MVC の入口の受付係

というイメージで十分だった。

ブラウザから来た HTTP リクエストを受けて、

  • どの Controller に渡すか
  • どうレスポンスを返すか

を振り分ける中心。


AOP を追加して確認したこと

AOP 用のクラス LoggingAspect を作った。

@Aspect
@Component
class LoggingAspect {

    @Before("execution(* com.example.springtodo.TodoService.getTodos(..))")
    fun beforeGetTodos() {
        println("AOP before: getTodos")
    }

    @After("execution(* com.example.springtodo.TodoService.getTodos(..))")
    fun afterGetTodos() {
        println("AOP after: getTodos")
    }
}

/todos を叩くと、ログはこうなった。

  • AOP before: getTodos
  • getTodos called
  • AOP after: getTodos

これで、

Service 本体を書き換えずに、前後へ共通ログを差し込める

ことを確認できた。


(..) は何か

AOP のこの部分は分かりづらかった。

execution(* com.example.springtodo.TodoService.getTodos(..))

ここで (..) は、

引数は何でもよい

という意味。

今回は getTodos() は引数なしだが、AOP の指定では (..) にしておくことで、引数の細かい型や数を意識せず指定できる。


IntelliJ に「関数は使用されません」と出た件

beforeGetTodos()afterGetTodos() に対して、エディタ上では「関数は使用されません」と出た。

これは異常ではなく、IntelliJ の静的解析からは コード上で直接呼ばれていない ように見えるため。

実際には、Spring AOP が実行時にアノテーションを見て呼んでいる。
つまり、

  • コード上から直接呼ばれない
  • でも Spring 実行時には使われる

という関数だった。


ログレベル設定を確認したこと

application.properties に次を追加した。

logging.level.com.example.springtodo=DEBUG

その上で TodoService に DEBUG ログも追加した。

fun getTodos(): List<String> {
    logger.debug("debug: entering getTodos")
    logger.info("getTodos called")
    return todoRepository.findAll()
}

/todos を叩くと、次のように出た。

  • AOP before: getTodos
  • DEBUG ... debug: entering getTodos
  • INFO ... getTodos called
  • AOP after: getTodos

これで、ログ設定が効いていることを確認できた。


「DEBUG 以上出す」の意味

logging.level.com.example.springtodo=DEBUG は、
DEBUG と、それより重要なログを出すという意味。

ログレベルの順番はこう理解した。

  • TRACE
  • DEBUG
  • INFO
  • WARN
  • ERROR

DEBUG を設定すると、出るのは次。

  • DEBUG
  • INFO
  • WARN
  • ERROR

出ないのは TRACE。

つまり、設定したレベル以上の重要度のログが表示されるという意味だった。


Git で残してよかったこと

今回は学習過程を Git で残した。

よかった点は次のとおり。

  • どの段階で何を追加したかが分かる
  • Controller → Service → Repository の変化が見える
  • Logging や AOP を入れたタイミングが追える
  • README をあとから足しても流れが崩れない

GUI ツール(Fork)に repo が出てこない場面もあったが、git init / add / commit 自体は問題なくできていたので、学習を止めずに進めた。


今回つまずいたこと

1. Java 17 問題

Spring Boot プロジェクトは Java 17 を要求していたが、Gradle が見つけられず起動できなかった。

対処としては、システム全体には Java を入れず、プロジェクト配下にローカル JDK を置いて解決した。


2. Spring Boot と Kotlin の役割の違いが最初あいまいだった

Kotlin は言語、Spring はフレームワーク、と整理したらかなり楽になった。


3. AOP の execution(...) の書き方が読みにくい

@Before / @After(..) の意味を分解すると理解しやすかった。


4. 「unused」と出るのに AOP が動くのが不思議だった

静的解析と実行時の動きの違いだと分かると納得できた。


今回の学習で一番大きかった理解

今回いちばん大きかったのは、
Spring はアノテーションとコンテナでクラス同士をつなぎながら、Web アプリの流れを組み立てている
という感覚が少し分かったこと。

最初は、

  • @RestController
  • @Service
  • @Repository
  • @Aspect

などがただの「おまじない」に見えた。

でも実際に触ると、

  • Spring がそれらを見つける
  • Bean として管理する
  • 依存関係を解決する
  • HTTP リクエストを Controller に渡す
  • AOP で処理の前後に割り込む

という流れが少し見えるようになった。


今なら説明できること

今の自分なら、少なくとも次は説明できる。

  • Spring Boot は組み込み Tomcat で Web アプリを起動できる
  • main が起点で、runApplication で Spring Boot が立ち上がる
  • Controller は HTTP リクエストの入口
  • Service は処理の中身
  • Repository はデータ取得担当
  • @RestController@GetMapping で URL と処理を結びつけられる
  • Logging は logger.debug() / logger.info() と設定ファイルで制御できる
  • AOP は本体の外から共通処理を差し込める

面談用に短くまとめるなら

Kotlin + Spring Boot で最小の REST API を作成し、GET /helloGET /todos を動かした。
TodoControllerTodoServiceTodoRepository に役割を分け、TodoService にログを追加して application.properties でログレベル設定を確認した。
さらに LoggingAspect を作成して、TodoService.getTodos() の前後で AOP によるログ処理が動くことを確認した。
今回は DB や認証は入れず、スキルマップ達成のために Spring Boot の基本構成、Logging、AOP の理解に絞って手を動かした。


今後、余裕があればやりたいこと

今回はここで十分だと思っているが、次に少しだけ広げるならこのあたり。

  • POST /todos を追加する
  • data class Todo(...) を使って文字列ではなくオブジェクトを返す
  • Repository を固定値ではなくメモリの List にする
  • AOP を println ではなく logger にする
  • 例外時のログを追加する

ただし、今回のスキルマップ達成という目的に対しては、ここまででかなり十分。


まとめ

今回の学習で分かったのは、Spring は「難しいもの全部」ではなく、
起動 → Controller → Service → Repository → Logging → AOP
と順番に分けるとかなり理解しやすいということだった。

最初から全部理解しようとすると重いが、最小の REST API を1本作って、ログと AOP を順に足していくと、
Spring がどういう役割分担で動いているか が少しずつ見えてくる。

今回は「深く極める」のではなく、実際に少し動かして、説明できる状態になるという目的だった。
その意味では、かなりちょうどよい学習になったと思う。

コメント

タイトルとURLをコピーしました