Spring Boot와 Kotlin으로 RESTful API 만들기
Java 개발자의 관점에서 Spring Boot와 Kotlin, 그리고 JPA를 사용하여 간단한 상품(Product) 관리 RESTful API 만들어보기.
1. 프로젝트 시작하기
가장 먼저 start.spring.io를 통해 프로젝트를 생성합니다.
- Project: Gradle - Kotlin
- Language: Kotlin
- Spring Boot: 3.x.x (최신 안정 버전)
- Dependencies:
Spring Web: RESTful API를 만들기 위한 필수 의존성Spring Data JPA: 데이터베이스 연동을 위한 ORMH2 Database: 개발 및 테스트용 인메모리 데이터베이스
설정이 완료되면 GENERATE 버튼을 눌러 프로젝트를 다운로드하고 IDE(IntelliJ IDEA 권장)에서 열어줍니다.
2. JPA Entity를 Kotlin으로 작성하기
Java에서 Lombok의 @Data나 @Getter, @Setter로 만들던 Entity 클래스를 Kotlin에서는 data class 하나로 훨씬 간결하게 표현할 수 있습니다.
Product Entity를 만들어 보겠습니다.
src/main/kotlin/.../domain/Product.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.productapi.domain
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
@Entity
class Product(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null, // DB에 저장된 후에 ID가 할당되므로 nullable 'var'
var name: String,
var price: Int
)
Java 코드와 비교했을 때 눈에 띄는 Kotlin의 특징들을 살펴보겠습니다.
data class vs class
data class로 선언하면 컴파일러가 equals(), hashCode(), toString(), copy()와 같은 메서드를 자동으로 생성해 줍니다. Lombok이 해주던 역할을 언어 차원에서 기본으로 지원하는 셈이죠. 하지만 JPA Entity는 프록시 기술과의 충돌을 피하기 위해 final이 아니어야 합니다. Spring Boot는 kotlin-jpa 플러그인을 통해 data class의 final을 자동으로 해제해주므로 걱정 없이 사용할 수 있습니다. 일반 class로 작성해도 무방합니다.
var vs val
var(variable): 변경 가능한 변수 (Java의 일반 필드)val(value): 변경 불가능한 읽기 전용 변수 (Java의final필드) JPA Entity의 속성은 변경될 수 있으므로 대부분var로 선언합니다.
Null Safety (?)
Kotlin은 타입 시스템을 통해 NullPointerException을 컴파일 시점에 방지합니다. id 필드를 Long?으로 선언한 것은 “이 필드는 null 값을 가질 수 있다”는 것을 명시적으로 나타냅니다. 새로 생성된 객체는 DB에 저장되기 전까지 id가 없으므로 nullable 타입이 적합합니다.
3. Repository 생성하기
Repository 계층은 Java로 작성할 때와 거의 100% 동일합니다. interface를 만들고 JpaRepository를 상속받기만 하면 됩니다.
src/main/kotlin/.../repository/ProductRepository.kt```kotlin package com.example.productapi.repository
import com.example.productapi.domain.Product import org.springframework.data.jpa.repository.JpaRepository
interface ProductRepository : JpaRepository<Product, Long>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## 4. DTO와 Controller 만들기
Controller에서는 클라이언트와 직접 데이터를 주고받으므로, Entity를 그대로 노출하기보다는 DTO(Data Transfer Object)를 사용하는 것이 좋습니다. DTO 역시 `data class`로 간결하게 만들 수 있습니다.
#### DTO (Data Transfer Objects)```kotlin
// 요청 DTO: 상품 생성 시 클라이언트가 보낼 데이터
data class CreateProductRequest(
val name: String,
val price: Int
)
// 응답 DTO: 서버가 클라이언트에게 보낼 데이터
data class ProductResponse(
val id: Long,
val name: String,
val price: Int
)
Controller
이제 실제 API 엔드포인트를 만들어 보겠습니다. Java Spring에서 사용하던 어노테이션(@RestController, @RequestMapping, @PostMapping 등)을 그대로 사용하면 됩니다.
src/main/kotlin/.../controller/ProductController.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.example.productapi.controller
import com.example.productapi.domain.Product
import com.example.productapi.repository.ProductRepository
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.net.URI
@RestController
@RequestMapping("/api/products")
class ProductController(
private val productRepository: ProductRepository // 생성자 주입
) {
// 모든 상품 조회
@GetMapping
fun getAllProducts(): ResponseEntity<List<ProductResponse>> {
val products = productRepository.findAll()
.map { it.toResponse() } // it은 람다식의 기본 인자 이름
return ResponseEntity.ok(products)
}
// 상품 생성
@PostMapping
fun createProduct(@RequestBody request: CreateProductRequest): ResponseEntity<ProductResponse> {
val product = Product(name = request.name, price = request.price)
val savedProduct = productRepository.save(product)
return ResponseEntity.created(URI.create("/api/products/${savedProduct.id}"))
.body(savedProduct.toResponse())
}
}
// 확장 함수(Extension Function)를 이용한 DTO 변환 로직
private fun Product.toResponse(): ProductResponse {
return ProductResponse(
id = this.id!!, // non-null 단언. id가 null이 아님을 보증
name = this.name,
price = this.price
)
}
Java 경험을 살린 Kotlin 코드 작성법
- 생성자 주입: Kotlin에서는 클래스 선언부에
private val로 의존성을 선언하는 것만으로 간결한 생성자 주입이 완성됩니다. - 확장 함수 (Extension Function):
Product.toResponse()와 같이 기존 클래스에 새로운 함수를 추가하는 기능입니다. 이를 통해 Entity 내부에 DTO 변환 로직을 넣지 않고도, 마치 Entity의 멤버 함수인 것처럼 깔끔하게 변환 코드를 분리할 수 있습니다. - Non-null 단언 (
!!):this.id!!는 “id가 절대 null이 아님을 내가 보증한다”는 의미입니다. 저장된 후의 Product 객체는 항상 id를 가지므로 안전하게 사용할 수 있습니다.
5. 실행 및 테스트
애플리케이션을 실행하고 curl이나 Postman 같은 도구로 API를 테스트해 봅시다.
1. 상품 생성 (POST)
1
2
3
curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name": "코틀린 인 액션", "price": 32000}'
응답으로
201 Created와 생성된 상품 정보가 반환됩니다.
2. 모든 상품 조회 (GET)
1
curl http://localhost:8080/api/products
응답으로
200 OK와 함께 상품 목록 배열이 반환됩니다.[{"id":1,"name":"코틀린 인 액션","price":32000}]
