Kotlin: KotlinによるSpring Boot開発
最終更新:2026-08-26
Spring BootとKotlinは、バックエンドのマイクロサービスにとって最高の組み合わせです。Charlieは、リクエスト/レスポンスの本文にデータクラスを使用し、ノンブロッキングAPIにはsuspendのコントローラーメソッドを採用し、コードをよりKotlinらしい表現にするために拡張関数を活用しています。
1. 学習内容
- Spring Boot + Kotlin の設定
- コントローラー:
@RestController+ データクラスのリクエスト/レスポンス本文 - コルーチンのサポート:
suspendコントローラのメソッド - JPA + Kotlin:
kotlin-jpaコンパイラプラグイン - Charlieの実践ガイド:OrderProcessor マイクロサービス
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 のリクエスト処理チェーン
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許容性、およびデフォルト値の処理がすべて失敗します)。
📖 まとめ
- Spring Boot + Kotlin の設定には、
kotlin-springおよびkotlin-jpaプラグインが必要です - リクエスト/レスポンス用DTOのデータクラス:1行で30行以上のJavaコードを置き換え
suspendコントローラーメソッドとWebFluxによるノンブロッキングAPIの実現kotlin-jpaプラグインは、エンティティ用の引数なしコンストラクタを生成しますtoResponse()のような拡張関数により、ドメインモデルとAPIモデルを分離する- WebFlux とコルーチンによるスループットは、MVC とスレッドプールをはるかに上回る
📝 練習問題
- 初心者 (⭐):データクラスを使用して
CreateUserRequestとUserResponseを定義し、Spring Boot のコントローラーを作成してください。ヒント:@RestController+@PostMapping - 中級 (⭐⭐):
suspendコントローラのメソッドを実装し、suspendサービス関数を呼び出して注文を取得するようにします。ヒント:suspend fun getOrder(@PathVariable id: String) - 上級 (⭐⭐⭐): 例外処理やバリデーションを含め、完全なCRUD REST API + コルーチン + R2DBC(またはリポジトリのシミュレーション)を実装してください。ヒント:
@RestControllerAdvice+@Valid