[Kotlin] Spring Boot JPA - CRUD 구현
Spring BootDDD 구조로 간단하게 CRUD 기능 구현
build.gradle.kts
JPA, QueryDSL, AllOpen, NoArg, H2 설정
plugins {
val kotlinVersion = "1.6.21"
kotlin("jvm") version kotlinVersion
kotlin("plugin.spring") version kotlinVersion
// querydsl
kotlin("kapt") version kotlinVersion
// Kotlin JPA 개발 Guide
kotlin("plugin.jpa") version kotlinVersion
// allOpen에서 지정한 어노테이션으로 만든 클래스에 open 키워드 적용
kotlin("plugin.allopen") version kotlinVersion
// 인자 없는 기본 생성자를 자동 생성
// - Hibernate가 사용하는 Reflection API에서 Entity를 만들기 위해 인자 없는 기본 생성자가 필요함
kotlin("plugin.noarg") version kotlinVersion
}
val querydslVersion = "5.0.0"
allOpen {
annotation("javax.persistence.Entity")
}
noArg {
annotation("javax.persistence.Entity")
}
dependencies {
// JPA
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
// querydsl
implementation("com.querydsl:querydsl-jpa:$querydslVersion")
kapt("com.querydsl:querydsl-apt:$querydslVersion:jpa")
kapt("org.springframework.boot:spring-boot-configuration-processor")
// Dev
testImplementation(kotlin("test"))
runtimeOnly("com.h2database:h2")
runtimeOnly("org.springframework.boot:spring-boot-devtools")
}
application.properties
Query Log 보기 좋게 설정
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.highlight_sql=true
Domain
package com.example.demo.crud.domain
import javax.persistence.*
@Entity
class Customer (
@Column(length = 30, nullable = false)
var nickname: String,
@Column
var age: Int,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
) {}
Infrastructure
package com.example.demo.crud.infra
import com.example.demo.crud.domain.Customer
import org.springframework.data.jpa.repository.JpaRepository
interface CustomerRepository: JpaRepository<Customer, Long> {
}
Application
package com.example.demo.crud.application
import com.example.demo.crud.domain.Customer
import com.example.demo.crud.infra.CustomerRepository
import com.example.demo.crud.ui.CustomerRequest
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
@Service
class CustomerService @Autowired constructor (val customerRepository: CustomerRepository) {
fun save(request: CustomerRequest): Long? {
return customerRepository.save(request.toEntity()).id
}
fun findCustomer(id: Long): Customer {
return customerRepository.findById(id).get()
}
fun findCustomerList(): List<Customer> {
return customerRepository.findAll()
}
fun update(id: Long, request: CustomerRequest): Long? {
val customer = customerRepository.findById(id).get()
customer.nickname = request.nickname
customer.age = request.age
customerRepository.save(customer)
return customer.id
}
fun delete(id: Long) {
customerRepository.deleteById(id)
}
}
Presentation
package com.example.demo.crud.ui
import com.example.demo.crud.domain.Customer
data class CustomerRequest(
val nickname: String,
val age: Int,
val id: Long? = null
) {
fun toEntity(): Customer = Customer(
nickname = nickname,
age = age,
id = id
)
}
package com.example.demo.crud.ui
import com.example.demo.crud.application.CustomerService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("customer")
class CustomerController @Autowired constructor(private val customerService: CustomerService) {
@GetMapping("/save")
fun save(request: CustomerRequest): ResponseEntity<Any> {
val save = customerService.save(request)
return ResponseEntity.ok().body(save)
}
@GetMapping("/{id}")
fun findCustomer(@PathVariable id: Long): ResponseEntity<Any> {
val customer = customerService.findCustomer(id)
return ResponseEntity.ok().body(customer)
}
@GetMapping("/list")
fun findCustomerList(): ResponseEntity<Any> {
return ResponseEntity.ok().body(customerService.findCustomerList())
}
@PutMapping("/{id}")
fun update(@PathVariable id: Long, @RequestBody request: CustomerRequest): ResponseEntity<Any> {
println("updatedddd")
val customer = customerService.update(id, request)
return ResponseEntity.ok().body(customer)
}
@DeleteMapping("/{id}")
fun delete(@PathVariable id: Long): ResponseEntity<Any> {
customerService.delete(id)
return ResponseEntity.ok().body(true)
}
}
TEST
package com.example.demo
import com.example.demo.crud.domain.Customer
import com.fasterxml.jackson.databind.ObjectMapper
import kotlin.test.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
@SpringBootTest
@AutoConfigureMockMvc
class CustomerControllerTest @Autowired constructor (val mvc: MockMvc, val mapper: ObjectMapper) {
@Test
fun crudTest() {
var nickname = "TEST"
var age = 10
mvc.perform(
get("/customer/save")
.param("nickname", nickname)
.param("age", age.toString()))
.andExpect(status().isOk)
mvc.perform(
get("/customer/1"))
.andExpect(status().isOk)
nickname = "LOVE"
age = 20
val content = mapper.writeValueAsString(Customer(nickname, age))
mvc.perform(
put("/customer/{id}", 1)
.content(content)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
mvc.perform(get("/customer/list"))
.andExpect(status().isOk)
mvc.perform(delete("/customer/{id}", 1))
.andExpect(status().isOk)
}
}