Kotlin, Spring Boot 3 (JDK 17), Gradle.kts, κ·Έλ¦¬κ³ μ΅μ κ°λ° κ·Όν©μ λ§μΆ° Clean Architectureμ λ μ΄μ΄λ μν€ν μ²λ₯Ό μ μ©ν κ²μν(Board) μλΉμ€μ ν΅μ¬ μ½λλ₯Ό μ κ³΅ν΄ λ립λλ€.
μ¬κΈ°μλ REST APIλ₯Ό μ¬μ©νμ¬ κ²μν κΈ°λ₯μ ꡬνν©λλ€.
build.gradle.kts)Kotlin, Spring Boot 3, JPA(λ°μ΄ν°λ² μ΄μ€), κ·Έλ¦¬κ³ JSON μ²λ¦¬λ₯Ό μν μμ‘΄μ±μ μ€μ ν©λλ€.
// build.gradle.kts
plugins {
// Spring Boot νλ¬κ·ΈμΈ
id("org.springframework.boot") version "3.2.0"
id("io.spring.dependency-management") version "1.1.4"
// Kotlin νλ¬κ·ΈμΈ
kotlin("jvm") version "1.9.20"
kotlin("plugin.spring") version "1.9.20"
kotlin("plugin.jpa") version "1.9.20" // JPA κ΄λ ¨ μ€μ μλν
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
dependencies {
// Spring Boot Web (REST API μ§μ)
implementation("org.springframework.boot:spring-boot-starter-web")
// Kotlin Coroutines μ§μ (λΉλκΈ° μ²λ¦¬μ μ 리)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
// Spring Data JPA (DB μ°λ)
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
// Kotlin JSON μ²λ¦¬ (Jackson)
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
// H2 Database (κ°λ° λ° ν
μ€νΈμ© μΈλ©λͺ¨λ¦¬ DB)
runtimeOnly("com.h2database:h2")
// ν
μ€νΈ μμ‘΄μ±
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
application.yml)H2 λ°μ΄ν°λ² μ΄μ€ μ€μ μ ν¬ν¨ν κΈ°λ³Έ μ€μ μ YAML νμμΌλ‘ ν©λλ€.
# src/main/resources/application.yml
spring:
application:
name: kotlin-board-app
datasource:
# H2 Database μ€μ
url: jdbc:h2:mem:testdb
driverClassName: org.h2.Driver
username: sa
password:
# JPA μ€μ
jpa:
hibernate:
ddl-auto: update # μ ν리μΌμ΄μ
μ€ν μ μν°ν° κΈ°λ°μΌλ‘ ν
μ΄λΈ μλ μμ±/μ
λ°μ΄νΈ
show-sql: true # SQL 쿼리 λ‘κ·Έ μΆλ ₯
properties:
hibernate:
format_sql: true # SQL ν¬λ§·ν
# H2 Web Console νμ±ν (κ°λ° μ DB νμΈμ©)
h2:
console:
enabled: true
path: /h2-console
κ²μνμ ν΅μ¬ 4κ°μ§ λ μ΄μ΄(DTO, Entity, Repository, Service, Controller)λ₯Ό Kotlin μ½λλ‘ κ΅¬νν©λλ€.
ν΄λΌμ΄μΈνΈμ μλ² κ°μ λ°μ΄ν° μ μ‘μ μ¬μ©λλ λ°μ΄ν° ν΄λμ€μ λλ€.
// DTOs for client communication
data class BoardCreateRequest(
val title: String,
val content: String,
val author: String
)
data class BoardUpdateRequest(
val title: String?,
val content: String?
)
data class BoardResponse(
val id: Long,
val title: String,
val content: String,
val author: String,
val createdAt: String
)
λ°μ΄ν°λ² μ΄μ€μ ν μ΄λΈμ λνλ΄λ JPA μν°ν°μ λλ€.
// src/main/kotlin/com/example/entity/Board.kt
package com.example.entity
import jakarta.persistence.*
import java.time.LocalDateTime
@Entity
data class Board(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0, // μ΄κΈ°κ° 0, DBμμ μλ μμ±
var title: String,
@Lob // Content νλλ₯Ό κΈΈκ² μ μ₯νκΈ° μν΄ μ¬μ©
var content: String,
val author: String,
val createdAt: LocalDateTime = LocalDateTime.now(),
var updatedAt: LocalDateTime? = null
) {
// DTO λ³νμ μν νμ₯ ν¨μ
fun toResponse() = BoardResponse(
id = id,
title = title,
content = content,
author = author,
createdAt = createdAt.toString()
)
}
JPAλ₯Ό μμλ°μ λ°μ΄ν°λ² μ΄μ€μ μ κ·Όνλ μΈν°νμ΄μ€μ λλ€.
// src/main/kotlin/com/example/repository/BoardRepository.kt
package com.example.repository
import com.example.entity.Board
import org.springframework.data.jpa.repository.JpaRepository
// Spring Data JPAλ μΈν°νμ΄μ€ μ μλ§μΌλ‘ CRUD λ©μλλ₯Ό μλμΌλ‘ ꡬνν΄μ€λλ€.
interface BoardRepository : JpaRepository<Board, Long> {
// μΆκ°μ μΈ μΏΌλ¦¬ λ©μλκ° νμνλ©΄ μ¬κΈ°μ μ μΈν©λλ€. μ:
// fun findByAuthor(author: String): List<Board>
}
Repositoryλ₯Ό μ¬μ©νμ¬ ν΅μ¬ λΉμ¦λμ€ λ‘μ§μ μ²λ¦¬νλ κ³μΈ΅μ λλ€.
// src/main/kotlin/com/example/service/BoardService.kt
package com.example.service
import com.example.dto.BoardCreateRequest
import com.example.dto.BoardResponse
import com.example.dto.BoardUpdateRequest
import com.example.entity.Board
import com.example.repository.BoardRepository
import jakarta.transaction.Transactional
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import java.time.LocalDateTime
@Service
class BoardService(
private val boardRepository: BoardRepository
) {
// 1. κ²μκΈ μ 체 μ‘°ν (λͺ©λ‘)
fun getAllBoards(): List<BoardResponse> {
return boardRepository.findAll()
.map { it.toResponse() } // Entity -> Response DTO λ³ν
}
// 2. κ²μκΈ μμΈ μ‘°ν
fun getBoardById(id: Long): BoardResponse {
val board = boardRepository.findByIdOrNull(id)
?: throw NoSuchElementException("κ²μκΈ ID ${id}λ₯Ό μ°Ύμ μ μμ΅λλ€.")
return board.toResponse()
}
// 3. κ²μκΈ μμ±
fun createBoard(request: BoardCreateRequest): BoardResponse {
val newBoard = Board(
title = request.title,
content = request.content,
author = request.author
)
val savedBoard = boardRepository.save(newBoard)
return savedBoard.toResponse()
}
// 4. κ²μκΈ μμ
@Transactional // JPA λ³κ²½ κ°μ§λ₯Ό μν΄ νΈλμμ
νμ
fun updateBoard(id: Long, request: BoardUpdateRequest): BoardResponse {
val board = boardRepository.findByIdOrNull(id)
?: throw NoSuchElementException("κ²μκΈ ID ${id}λ₯Ό μ°Ύμ μ μμ΅λλ€.")
// Kotlinμ μμ νΈμΆ μ°μ°μ(?)μ Elvis μ°μ°μ(?:)λ₯Ό νμ©ν κ°κ²°ν μ
λ°μ΄νΈ
request.title?.let { board.title = it }
request.content?.let { board.content = it }
board.updatedAt = LocalDateTime.now()
// @Transactional λλΆμ save()λ₯Ό νΈμΆν νμκ° μμ΅λλ€.
return board.toResponse()
}
// 5. κ²μκΈ μμ
fun deleteBoard(id: Long) {
if (!boardRepository.existsById(id)) {
throw NoSuchElementException("κ²μκΈ ID ${id}λ₯Ό μ°Ύμ μ μμ΅λλ€.")
}
boardRepository.deleteById(id)
}
}
ν΄λΌμ΄μΈνΈμ HTTP μμ²μ λ°μ Serviceμ μ λ¬νκ³ μλ΅μ λ°ννλ REST API μΈν°νμ΄μ€μ λλ€.
// src/main/kotlin/com/example/controller/BoardController.kt
package com.example.controller
import com.example.dto.BoardCreateRequest
import com.example.dto.BoardResponse
import com.example.dto.BoardUpdateRequest
import com.example.service.BoardService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/boards")
class BoardController(
private val boardService: BoardService
) {
// GET /api/boards : μ 체 λͺ©λ‘ μ‘°ν
@GetMapping
fun getAllBoards(): ResponseEntity<List<BoardResponse>> {
val boards = boardService.getAllBoards()
return ResponseEntity.ok(boards)
}
// GET /api/boards/{id} : μμΈ μ‘°ν
@GetMapping("/{id}")
fun getBoardById(@PathVariable id: Long): ResponseEntity<BoardResponse> {
val board = boardService.getBoardById(id)
return ResponseEntity.ok(board)
}
// POST /api/boards : κ²μκΈ μμ±
@PostMapping
fun createBoard(@RequestBody request: BoardCreateRequest): ResponseEntity<BoardResponse> {
val newBoard = boardService.createBoard(request)
return ResponseEntity
.status(HttpStatus.CREATED) // HTTP 201 μλ΅
.body(newBoard)
}
// PUT /api/boards/{id} : κ²μκΈ μμ
@PutMapping("/{id}")
fun updateBoard(
@PathVariable id: Long,
@RequestBody request: BoardUpdateRequest
): ResponseEntity<BoardResponse> {
val updatedBoard = boardService.updateBoard(id, request)
return ResponseEntity.ok(updatedBoard)
}
// DELETE /api/boards/{id} : κ²μκΈ μμ
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) // HTTP 204 μλ΅ (μ±κ³΅μ μΌλ‘ μμ λμμ§λ§ λ³Έλ¬Έ μμ)
fun deleteBoard(@PathVariable id: Long) {
boardService.deleteBoard(id)
}
}
μ΄ μ½λλ λ€μκ³Ό κ°μ μ΅μ Kotlin/Spring Boot κ°λ° νΈλ λλ₯Ό λ°μνκ³ μμ΅λλ€.
build.gradle λμ Kotlin κΈ°λ°μ build.gradle.ktsλ₯Ό μ¬μ©νμ¬ λ κ°λ ₯ν νμ
μμ μ±κ³Ό IDE μ§μμ λ°μ΅λλ€.@Autowired λμ Kotlin ν΄λμ€μ μ£Ό μμ±μλ₯Ό ν΅ν΄ μμ‘΄μ±μ μ£Όμ
νμ¬ μ½λκ° κ°κ²°νκ³ λΆλ³μ±(Immutability)μ μ μ§νκΈ° μ½μ΅λλ€. (μ: class BoardService(private val boardRepository: BoardRepository))data class): DTOμ Entityμ data classλ₯Ό μ¬μ©νμ¬ equals(), hashCode(), toString() λ±μ μλμΌλ‘ μμ±νκ³ μ½λλ₯Ό μ΅μνν©λλ€.val (Immutable)μ κΈ°λ³ΈμΌλ‘ μ¬μ©ν©λλ€.BoardUpdateRequestμ νλμ String?λ₯Ό μ¬μ©νμ¬ Null μμ μ±μ λͺ
νν ν©λλ€.request.title?.let { ... } (μμ νΈμΆ + let μ€μ½ν ν¨μ)κ³Ό κ°μ Kotlin κ³ μ μ λ¬Έλ²μΌλ‘ Null 체ν¬λ₯Ό κ°κ²°νκ² μ²λ¦¬ν©λλ€.Board μν°ν° λ΄λΆμ toResponse()μ κ°μ νμ₯ ν¨μλ₯Ό μ μνμ¬, μν°ν°λ₯Ό DTOλ‘ λ³ννλ λ‘μ§μ κΉλνκ² μΊ‘μννμ΅λλ€.