πŸ“ Kotlin Spring Boot 3 κ²Œμ‹œνŒ (Board) μ½”λ“œ μ˜ˆμ‹œ

devdoΒ·2025λ…„ 12μ›” 1일

μ½”ν‹€λ¦°

λͺ©λ‘ 보기
3/4

πŸ“ Kotlin Spring Boot 3 κ²Œμ‹œνŒ (Board) μ½”λ“œ μ˜ˆμ‹œ

Kotlin, Spring Boot 3 (JDK 17), Gradle.kts, 그리고 μ΅œμ‹  개발 근황에 맞좰 Clean Architecture와 λ ˆμ΄μ–΄λ“œ μ•„ν‚€ν…μ²˜λ₯Ό μ μš©ν•œ κ²Œμ‹œνŒ(Board) μ„œλΉ„μŠ€μ˜ 핡심 μ½”λ“œλ₯Ό μ œκ³΅ν•΄ λ“œλ¦½λ‹ˆλ‹€.

μ—¬κΈ°μ„œλŠ” REST APIλ₯Ό μ‚¬μš©ν•˜μ—¬ κ²Œμ‹œνŒ κΈ°λŠ₯을 κ΅¬ν˜„ν•©λ‹ˆλ‹€.


1. βš™οΈ ν”„λ‘œμ νŠΈ μ„€μ • (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"
    }
}

2. πŸ“ Application μ„€μ • (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

3. 🎯 핡심 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 (Board Module)

κ²Œμ‹œνŒμ˜ 핡심 4κ°€μ§€ λ ˆμ΄μ–΄(DTO, Entity, Repository, Service, Controller)λ₯Ό Kotlin μ½”λ“œλ‘œ κ΅¬ν˜„ν•©λ‹ˆλ‹€.

3.1. DTO (Data Transfer Object)

ν΄λΌμ΄μ–ΈνŠΈμ™€ μ„œλ²„ κ°„μ˜ 데이터 전솑에 μ‚¬μš©λ˜λŠ” 데이터 ν΄λž˜μŠ€μž…λ‹ˆλ‹€.

// 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
)

3.2. Entity (데이터 λͺ¨λΈ)

λ°μ΄ν„°λ² μ΄μŠ€μ˜ ν…Œμ΄λΈ”μ„ λ‚˜νƒ€λ‚΄λŠ” 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()
    )
}

3.3. Repository (데이터 μ ‘κ·Ό)

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>
}

3.4. Service (λΉ„μ¦ˆλ‹ˆμŠ€ 둜직)

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)
    }
}

3.5. Controller (μ™ΈλΆ€ 톡신)

ν΄λΌμ΄μ–ΈνŠΈμ˜ 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)
    }
}

4. πŸš€ 핡심 정리: μ΅œμ‹  Kotlin 개발 νŠΈλ Œλ“œ 적용 포인트

이 μ½”λ“œλŠ” λ‹€μŒκ³Ό 같은 μ΅œμ‹  Kotlin/Spring Boot 개발 νŠΈλ Œλ“œλ₯Ό λ°˜μ˜ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

  1. Gradle.kts (Kotlin DSL): Groovy 기반의 build.gradle λŒ€μ‹  Kotlin 기반의 build.gradle.ktsλ₯Ό μ‚¬μš©ν•˜μ—¬ 더 κ°•λ ₯ν•œ νƒ€μž… μ•ˆμ •μ„±κ³Ό IDE 지원을 λ°›μŠ΅λ‹ˆλ‹€.
  2. μƒμ„±μž μ£Όμž… (Primary Constructor Injection): @Autowired λŒ€μ‹  Kotlin 클래슀의 μ£Ό μƒμ„±μžλ₯Ό 톡해 μ˜μ‘΄μ„±μ„ μ£Όμž…ν•˜μ—¬ μ½”λ“œκ°€ κ°„κ²°ν•˜κ³  λΆˆλ³€μ„±(Immutability)을 μœ μ§€ν•˜κΈ° μ‰½μŠ΅λ‹ˆλ‹€. (예: class BoardService(private val boardRepository: BoardRepository))
  3. 데이터 클래슀 (data class): DTO와 Entity에 data classλ₯Ό μ‚¬μš©ν•˜μ—¬ equals(), hashCode(), toString() 등을 μžλ™μœΌλ‘œ μƒμ„±ν•˜κ³  μ½”λ“œλ₯Ό μ΅œμ†Œν™”ν•©λ‹ˆλ‹€.
  4. λΆˆλ³€μ„± 및 Null μ•ˆμ „μ„±:
    • val (Immutable)을 기본으둜 μ‚¬μš©ν•©λ‹ˆλ‹€.
    • BoardUpdateRequest의 ν•„λ“œμ— String?λ₯Ό μ‚¬μš©ν•˜μ—¬ Null μ•ˆμ „μ„±μ„ λͺ…ν™•νžˆ ν•©λ‹ˆλ‹€.
    • request.title?.let { ... } (μ•ˆμ „ 호좜 + let μŠ€μ½”ν”„ ν•¨μˆ˜)κ³Ό 같은 Kotlin 고유의 λ¬Έλ²•μœΌλ‘œ Null 체크λ₯Ό κ°„κ²°ν•˜κ²Œ μ²˜λ¦¬ν•©λ‹ˆλ‹€.
  5. JPA ν™•μž₯ ν•¨μˆ˜: Board μ—”ν‹°ν‹° 내뢀에 toResponse()와 같은 ν™•μž₯ ν•¨μˆ˜λ₯Ό μ •μ˜ν•˜μ—¬, μ—”ν‹°ν‹°λ₯Ό DTO둜 λ³€ν™˜ν•˜λŠ” λ‘œμ§μ„ κΉ”λ”ν•˜κ²Œ μΊ‘μŠν™”ν–ˆμŠ΅λ‹ˆλ‹€.
profile
μžλ°” μŠ€ν”„λ§ λ°±μ—”λ“œ κ°œλ°œμžμž…λ‹ˆλ‹€. 배운 것을 κΈ°λ‘ν•©λ‹ˆλ‹€.

0개의 λŒ“κΈ€