[2026] Kotlin Spring Boot | REST API 서버 만들기

[2026] Kotlin Spring Boot | REST API 서버 만들기

이 글의 핵심

Kotlin Spring Boot: REST API 서버 만들기. Spring Boot 프로젝트 설정·REST Controller.

들어가며

Spring Boot는 Java 기반이지만 Kotlin으로 작성해도 동일한 스타터·빈을 그대로 씁니다. build.gradle.kts에서 의존성을 선언하고, 서비스·리포지토리를 설계도(클래스)에 맞춰 나누는 흐름이 일반적입니다.

실무에서 마주한 현실

개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.

1. Spring Boot 프로젝트 설정

build.gradle.kts

아래 코드는 kotlin를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

plugins {
    kotlin("jvm") version "1.9.0"
    kotlin("plugin.spring") version "1.9.0"
    kotlin("plugin.jpa") version "1.9.0"
    id("org.springframework.boot") version "3.2.0"
}
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("com.h2database:h2")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
}

Application.kt

아래 코드는 kotlin를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

package com.example.demo
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class Application
fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

application.yml

아래 코드는 yaml를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

server:
  port: 8080
spring:
  datasource:
    url: jdbc:h2:mem:testdb
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

2. REST Controller

UserController

다음은 kotlin를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

package com.example.demo.controller
import com.example.demo.model.User
import com.example.demo.service.UserService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {
    
    @GetMapping
    fun getUsers(): List<User> {
        return userService.findAll()
    }
    
    @GetMapping("/{id}")
    fun getUser(@PathVariable id: Long): ResponseEntity<User> {
        val user = userService.findById(id)
        return if (user != null) {
            ResponseEntity.ok(user)
        } else {
            ResponseEntity.notFound().build()
        }
    }
    
    @PostMapping
    fun createUser(@RequestBody user: User): ResponseEntity<User> {
        val created = userService.save(user)
        return ResponseEntity.status(HttpStatus.CREATED).body(created)
    }
    
    @PutMapping("/{id}")
    fun updateUser(
        @PathVariable id: Long,
        @RequestBody user: User
    ): ResponseEntity<User> {
        val updated = userService.update(id, user)
        return if (updated != null) {
            ResponseEntity.ok(updated)
        } else {
            ResponseEntity.notFound().build()
        }
    }
    
    @DeleteMapping("/{id}")
    fun deleteUser(@PathVariable id: Long): ResponseEntity<Void> {
        val deleted = userService.delete(id)
        return if (deleted) {
            ResponseEntity.noContent().build()
        } else {
            ResponseEntity.notFound().build()
        }
    }
}

3. JPA Entity

User 엔티티

다음은 kotlin를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

package com.example.demo.model
import jakarta.persistence.*
@Entity
@Table(name = "users")
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    
    @Column(nullable = false)
    val name: String,
    
    @Column(nullable = false, unique = true)
    val email: String,
    
    val age: Int? = null
)

4. Repository

UserRepository

다음은 kotlin를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

package com.example.demo.repository
import com.example.demo.model.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface UserRepository : JpaRepository<User, Long> {
    
    fun findByEmail(email: String): User?
    
    fun findByNameContaining(keyword: String): List<User>
    
    fun findByAgeGreaterThan(age: Int): List<User>
}

5. Service Layer

UserService

다음은 kotlin를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

package com.example.demo.service
import com.example.demo.model.User
import com.example.demo.repository.UserRepository
import org.springframework.stereotype.Service
@Service
class UserService(private val userRepository: UserRepository) {
    
    fun findAll(): List<User> {
        return userRepository.findAll()
    }
    
    fun findById(id: Long): User? {
        return userRepository.findById(id).orElse(null)
    }
    
    fun save(user: User): User {
        return userRepository.save(user)
    }
    
    fun update(id: Long, user: User): User? {
        val existing = findById(id) ?: return null
        val updated = existing.copy(
            name = user.name,
            email = user.email,
            age = user.age
        )
        return userRepository.save(updated)
    }
    
    fun delete(id: Long): Boolean {
        return if (userRepository.existsById(id)) {
            userRepository.deleteById(id)
            true
        } else {
            false
        }
    }
}

6. 실전 예제

예제: Todo API

다음은 kotlin를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Todo.kt
@Entity
data class Todo(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val title: String,
    val completed: Boolean = false
)
// TodoRepository.kt
@Repository
interface TodoRepository : JpaRepository<Todo, Long> {
    fun findByCompleted(completed: Boolean): List<Todo>
}
// TodoService.kt
@Service
class TodoService(private val todoRepository: TodoRepository) {
    
    fun findAll(): List<Todo> = todoRepository.findAll()
    
    fun save(todo: Todo): Todo = todoRepository.save(todo)
    
    fun toggle(id: Long): Todo? {
        val todo = todoRepository.findById(id).orElse(null) ?: return null
        val updated = todo.copy(completed = !todo.completed)
        return todoRepository.save(updated)
    }
}
// TodoController.kt
@RestController
@RequestMapping("/api/todos")
class TodoController(private val todoService: TodoService) {
    
    @GetMapping
    fun getTodos(): List<Todo> = todoService.findAll()
    
    @PostMapping
    fun createTodo(@RequestBody todo: Todo): ResponseEntity<Todo> {
        val created = todoService.save(todo)
        return ResponseEntity.status(HttpStatus.CREATED).body(created)
    }
    
    @PutMapping("/{id}/toggle")
    fun toggleTodo(@PathVariable id: Long): ResponseEntity<Todo> {
        val updated = todoService.toggle(id)
        return if (updated != null) {
            ResponseEntity.ok(updated)
        } else {
            ResponseEntity.notFound().build()
        }
    }
}

정리

핵심 요약

  1. Spring Boot: Kotlin 완벽 지원
  2. data class: Entity 정의 간편
  3. 생성자 주입: 불변성 보장
  4. JpaRepository: CRUD 자동 제공
  5. ResponseEntity: HTTP 응답 제어

Kotlin의 장점

  • Null 안전성: ? 연산자
  • 간결한 문법: 보일러플레이트 감소
  • 확장 함수: 유틸리티 추가 용이
  • 코루틴: 비동기 처리 간편

다음 단계


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3