[Kotlin] Spring Boot JPA - CRUD 구현

Spring Boot

Language :

DDD 구조로 간단하게 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)
    }

}

민갤

Back-End Developer

백엔드 개발자입니다.