Kotlin: KotlinによるSpring Boot開発

最終更新:2026-08-26

Spring BootとKotlinは、バックエンドのマイクロサービスにとって最高の組み合わせです。Charlieは、リクエスト/レスポンスの本文にデータクラスを使用し、ノンブロッキングAPIにはsuspendのコントローラーメソッドを採用し、コードをよりKotlinらしい表現にするために拡張関数を活用しています。

1. 学習内容


2. ある建築家の実話

(1) 課題:Java Spring Boot の定型コード

Charlieが担当していたJavaのSpring Bootコントローラーでは、リクエストやレスポンスごとに30行以上のPOJOとゲッター/セッターが必要でした。CompletableFutureのネスト構造を用いた非同期APIは、メンテナンスが困難でした。

(2) Kotlin Spring Boot ソリューション

KOTLIN
// Java: 30+ lines for request DTO
public class CreateOrderRequest {
    private String customerId;
    private Double total;
    // getter/setter x 2 = 8 lines
}

// Kotlin: 1 line for request DTO
data class CreateOrderRequest(val customerId: String, val total: Double)

// Suspend controller method
@PostMapping
suspend fun createOrder(@RequestBody req: CreateOrderRequest): OrderResponse {
    return orderService.createOrder(req)  // Non-blocking!
}

データクラス と サスペンド を使用することで、Spring Boot のコード量を 60% 削減でき、非同期 API も同期コードと同じくらい読みやすくなります。


3. Spring Boot + Kotlin の設定

(1) ビルド.gradle.kts

KOTLIN
plugins {
    kotlin("jvm") version "1.9.22"
    kotlin("plugin.spring") version "1.9.22"   // Spring support
    kotlin("plugin.jpa") version "1.9.22"      // JPA no-arg
    kotlin("plugin.serialization") version "1.9.22"
    id("org.springframework.boot") version "3.2.1"
    id("io.spring.dependency-management") version "1.1.4"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-webflux")  // For coroutine support
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    runtimeOnly("org.postgresql:postgresql")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

(2) Kotlin Spring プラグインの概要

プラグイン 用途
kotlin-spring @Component@Transactional などのアノテーションが付いたクラスを自動的に開きます。
kotlin-jpa @Entity クラス用の引数なしコンストラクタを生成します
kotlin-serialization データクラスのJSONシリアライズを有効にします

4. REST コントローラー

(1) 基本コントローラ

KOTLIN
@RestController
@RequestMapping("/api/orders")
class OrderController(private val orderService: OrderService) {

    @GetMapping
    fun getAllOrders(): List<OrderResponse> = orderService.findAll()

    @GetMapping("/{id}")
    fun getOrder(@PathVariable id: String): OrderResponse =
        orderService.findById(id) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)

    @PostMapping
    fun createOrder(@RequestBody request: CreateOrderRequest): OrderResponse =
        orderService.create(request)
}

(2) リクエスト/レスポンスのデータクラス

KOTLIN
data class CreateOrderRequest(
    val customerId: String,
    val total: Double,
    val items: List<OrderItemRequest> = emptyList()
)

data class OrderItemRequest(
    val sku: String,
    val quantity: Int,
    val unitPrice: Double
)

data class OrderResponse(
    val id: String,
    val total: Double,
    val status: String,
    val customer: String
)

(3) JavaのDTOとKotlinのデータクラスの比較

次元 Java DTO Kotlin データクラス
コード行数 30行以上 3行
equals/hashCode 手動/Lombok 自動生成
不変性 手作業 デフォルト val
デフォルト値 メソッドのオーバーロード パラメータのデフォルト値
JSON マッピング Jackson アノテーション jackson-モジュール-kotlin auto

5. コルーチンのサポート

(1) コントローラのメソッドを一時停止する

KOTLIN
// Add webflux dependency for coroutine support
@RestController
@RequestMapping("/api/orders")
class OrderController(private val orderService: OrderService) {

    // Suspend controller: non-blocking, runs on Netty event loop
    @GetMapping("/{id}")
    suspend fun getOrder(@PathVariable id: String): OrderResponse {
        return orderService.findByIdAsync(id)
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
    }

    @PostMapping
    suspend fun createOrder(@RequestBody request: CreateOrderRequest): OrderResponse {
        return orderService.createAsync(request)
    }

    // Flow endpoint: streaming response
    @GetMapping("/stream")
    fun orderStream(): Flow<OrderResponse> = orderService.orderStream()
}

(2) Spring Boot のリクエスト処理チェーン

100%
flowchart TD
    A[HTTP Request] --> B[DispatcherServlet<br/>or Netty]
    B --> C[Controller<br/>@RestController]
    C --> D{suspend?}
    D -->|Yes| E[Coroutine Context<br/>Non-blocking]
    D -->|No| F[Servlet Thread<br/>Blocking]
    E --> G[Service Layer<br/>suspend functions]
    F --> G
    G --> H[Repository<br/>R2DBC / JPA]
    H --> I[Database]

(3) ブロッキングとノンブロッキングの比較

次元 ブロッキング (MVC) ノンブロッキング (WebFlux + コルーチン)
スレッドモデル リクエストごとに1つのスレッド イベントループ
同時実行数の上限 約200(スレッドプール) 約100,000以上(コルーチン)
コードスタイル 同期 サスペンド(syncのように読める)
データベース JPA(ブロッキング) R2DBC(リアクティブ)
スループット

6. JPA + Kotlin

(1) エンティティの定義

KOTLIN
@Entity
@Table(name = "orders")
class OrderEntity(
    @Id @GeneratedValue(strategy = GenerationType.UUID)
    val id: String = "",
    val total: Double = 0.0,
    val status: String = "PENDING",
    val customerId: String = "",
    @CreationTimestamp
    val createdAt: LocalDateTime = LocalDateTime.now()
)

// Repository
interface OrderJpaRepository : JpaRepository<OrderEntity, String> {
    fun findByCustomerId(customerId: String): List<OrderEntity>
    fun countByStatus(status: String): Long
}

(2) 引数なしのプラグイン

KOTLIN
// kotlin-jpa plugin auto-generates no-arg constructor for @Entity classes
// Without it, JPA cannot instantiate Kotlin classes (all have constructor params)

7. 完全な例:OrderProcessor マイクロサービス

▶ サンプル:OrderProcessorマイクロサービス

KOTLIN
// ============================================
// OrderProcessor - Spring Boot Microservice
// Feature: REST API + suspend + data class
// ============================================

// --- Domain ---
data class Order(val id: String, val total: Double, var status: String, val customerId: String)

// --- Request/Response ---
data class CreateOrderRequest(val customerId: String, val total: Double)
data class OrderResponse(val id: String, val total: Double, val status: String, val customerId: String)
data class UpdateStatusRequest(val status: String)

// --- Repository (simulated) ---
class OrderRepository {
    private val storage = mutableMapOf<String, Order>()

    fun save(order: Order): Order {
        storage[order.id] = order
        return order
    }

    fun findById(id: String): Order? = storage[id]

    fun findAll(): List<Order> = storage.values.toList()

    fun deleteById(id: String) { storage.remove(id) }
}

// --- Service ---
class OrderService(private val repo: OrderRepository) {
    private var idCounter = 0L

    fun create(request: CreateOrderRequest): Order {
        val order = Order("ORD-${++idCounter}", request.total, "PENDING", request.customerId)
        return repo.save(order)
    }

    fun findById(id: String): Order? = repo.findById(id)

    fun findAll(): List<Order> = repo.findAll()

    fun updateStatus(id: String, newStatus: String): Order {
        val order = repo.findById(id) ?: throw NoSuchElementException("Order $id not found")
        order.status = newStatus
        return repo.save(order)
    }

    fun delete(id: String) = repo.deleteById(id)
}

// --- Controller (simulated, requires Spring Boot runtime) ---
// @RestController
// @RequestMapping("/api/orders")
// class OrderController(private val orderService: OrderService) {
//     @GetMapping fun getAll() = orderService.findAll().map { it.toResponse() }
//     @GetMapping("/{id}") fun getOne(@PathVariable id: String) = orderService.findById(id)?.toResponse()
//     @PostMapping fun create(@RequestBody req: CreateOrderRequest) = orderService.create(req).toResponse()
// }

// --- Extension for mapping ---
fun Order.toResponse() = OrderResponse(id, total, status, customerId)

// --- Demo ---
fun main() {
    val repo = OrderRepository()
    val service = OrderService(repo)

    println("=== Spring Boot OrderProcessor API Demo ===\n")

    // POST /api/orders
    val order1 = service.create(CreateOrderRequest("CUST-001", 299.99))
    println("POST /api/orders -> ${order1.toResponse()}")

    val order2 = service.create(CreateOrderRequest("CUST-002", 15_000.00))
    println("POST /api/orders -> ${order2.toResponse()}")

    // GET /api/orders
    println("\nGET /api/orders -> ${service.findAll().map { it.toResponse() }}")

    // GET /api/orders/{id}
    println("GET /api/orders/ORD-1 -> ${service.findById("ORD-1")?.toResponse()}")

    // PATCH /api/orders/{id}/status
    val updated = service.updateStatus("ORD-1", "CONFIRMED")
    println("PATCH /api/orders/ORD-1/status -> ${updated.toResponse()}")

    // Summary
    val revenue = service.findAll().filter { it.status != "CANCELLED" }.sumOf { it.total }
    println("\nTotal Revenue: \$$revenue USD across ${service.findAll().size} orders")
}

出力:

TEXT 📖 参照専用
=== Spring Boot OrderProcessor API Demo ===

POST /api/orders -> OrderResponse(id=ORD-1, total=299.99, status=PENDING, customerId=CUST-001)
POST /api/orders -> OrderResponse(id=ORD-2, total=15000.0, status=PENDING, customerId=CUST-002)

GET /api/orders -> [OrderResponse(id=ORD-1, total=299.99, status=PENDING, customerId=CUST-001), OrderResponse(id=ORD-2, total=15000.0, status=PENDING, customerId=CUST-002)]
GET /api/orders/ORD-1 -> OrderResponse(id=ORD-1, total=299.99, status=PENDING, customerId=CUST-001)
PATCH /api/orders/ORD-1/status -> OrderResponse(id=ORD-1, total=299.99, status=CONFIRMED, customerId=CUST-001)

Total Revenue: $15299.99 USD across 2 orders

❓ よくある質問

Q Spring Boot では MVC と WebFlux のどちらを使うべきですか?
A 新規プロジェクトには WebFlux とコルーチンをおすすめします。— suspend のように、同期コードのように見えますが、実際にはノンブロッキングで動作します。 チームがJPAに慣れ親しんでおり、極端な並行処理を必要としない場合は、MVC + コルーチンでも問題ありません。
Q KotlinのデータクラスをJPAエンティティとして使用できますか?
A 可能ですが、制限があります。データクラスは本質的に不変であるのに対し、JPAではダーティチェックのために可変エンティティが必要です。 推奨:エンティティには通常の class および var プロパティを使用し、DTOにはデータクラスを使用してください。
Q kotlin-spring プラグインはどのような役割を果たしますか?
A Spring AOPではプロキシを作成する必要があるため(継承可能なクラスが必要となる)、@Component@Transactionalなどのアノテーションが付いたクラスを自動的にopenとしてマークします。
Q コントローラーメソッドのサスペンドにはWebFluxが必要ですか?
A はい。Spring MVCはsuspendをネイティブにはサポートしていません。WebFluxの依存関係が必要です。Spring 6.1以降ではサポートが改善されていますが、依然としてWebFluxの使用が推奨されます。
Q Spring Boot で Kotlin のコルーチンを使うにはどうすればよいですか?
A spring-boot-starter-webflux 依存関係を追加し、コントローラーのメソッドを suspend としてマークし、サービス層で suspend 関数を使用し、リポジトリには R2DBC またはコルーチン対応のリポジトリを使用します。
Q jackson-module-kotlin は必須ですか?
A 強く推奨されます。これがないと、Jackson は Kotlin のデータクラスを正しくデシリアライズできません(コンストラクタのパラメータ、null許容性、およびデフォルト値の処理がすべて失敗します)。

📖 まとめ


📝 練習問題

  1. 初心者 (⭐):データクラスを使用して CreateUserRequestUserResponse を定義し、Spring Boot のコントローラーを作成してください。ヒント:@RestController + @PostMapping
  2. 中級 (⭐⭐): suspend コントローラのメソッドを実装し、suspend サービス関数を呼び出して注文を取得するようにします。ヒント: suspend fun getOrder(@PathVariable id: String)
  3. 上級 (⭐⭐⭐): 例外処理やバリデーションを含め、完全なCRUD REST API + コルーチン + R2DBC(またはリポジトリのシミュレーション)を実装してください。ヒント: @RestControllerAdvice + @Valid

← 前へ | 次へ →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%