훈훈훈

Spring Boot :: Kotlin과 JPA를 사용하여 간단한 API 만들기 본문

Spring Framework/Kotlin

Spring Boot :: Kotlin과 JPA를 사용하여 간단한 API 만들기

훈훈훈 2020. 6. 14. 02:31

이번에는 Spring boot, JPA와 코틀린(Kotlin)을 사용하여 간단한 API를 만들어 보려고 한다.

 

현재 사내에서 API 서버를 스프링 부트와 코틀린을 사용하여 개발하고 있다.

구글 검색 시 자바에 비해 코틀린에 관한 내용은 많이 부족하다. ....그래서 시간 날떄  틈틈히 정리를 해보려고 한다.

 

사용한 기술은 아래와 같다.

-  Spring Boot

-  Kotlin

-  gradle

-  postgresql

-  JPA

 

IDE는 IntelliJ를 사용하였으며, 이클립스 환경이랑은 약간 차이가 날 수 있다.

이제 아래 코드를 보면서 살펴보자.

 

 

 

프로젝트 구조


먼저 프로젝트 구조는 아래와 같이 구성하였다.

파일 구성은 Controller, Service, DTO, Model, Repository 로 구성하였다.

각각의 역할은 그림 하단을 보자. 

 

 

-  Model  :  모델은 이름 그대로 데이터 모델을 객체화 시키는 영역

-  DTO  :  각 영역간에 데이터 교환을 위한 객체  (외부 요청에 대하여 다이렉트로 DB에 접근하는 행위는 해서는 안된다.)

-  Repositroy  :  데이터 베이스에 접근하는 영역

-  Service  :  DTO와 Controller 사이에 위치하며, 실제 비즈니스 로직이 처리되는 영역

-  Controller  :  외부 요청에 대하여 해당하는 Service로 요청하는 영역 

 

 

application.yml


어플리케이션 파일은 아래와 같이 설정하였다.

각 항목에 대한 자세한 내용은 나중에 JPA에 대하여 정리할때 하려고 한다.

 

spring:
  datasource:
    url: jdbc:postgresql://127.0.0.1:5432/[your database name]
    username: [postgres username]
    password: [postgres password]
    driverClassName: org.postgresql.Driver
  jpa:
    properties:
      hibernate:
        temp:
          use_jdbc_metadata_defaults: false
        show_sql: true
    generate-ddl: true
    hibernate:
      ddl-auto: update
      generate-ddl: true
      properties:
      temp:
        use_jdbc_metadata_defaults: false

 

위 파일에서 DB에 관한 설정을 진행한다.

ddl-auto 옵션을 활성화 시켰기 때문에 빌드 후, model에 있는 파일을 읽고 DB에 스키마를 자동으로 생성해준다.

 

생성된 DB는 인텔리제이나 postgres 서버에서 확인할 수 있다. 

 

 

설정에 대한 내용은 끝났으니 이제 각각 영역에 대한 코드를 살펴보자,

 

 

 

Model


package com.resteaxm.product

import java.time.OffsetDateTime
import javax.persistence.*

enum class Category {
    Phone, Laptop, Keyboard
}

@Entity
data class Product (
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val name: String,
    @Enumerated(EnumType.STRING)
    val category: Category,
    val createDateTime: OffsetDateTime = OffsetDateTime.now(),
    var updateDateTime: OffsetDateTime? = null
) {
    fun toReadProductDTO(): ReadProductDTO {
        return ReadProductDTO(
            id = id,
            name = name,
            category =  category
        )
    }

    fun toCreateProductDTO(): CreateProductDTO {
        return CreateProductDTO(
            name = name,
            category = category
        )
    }
}

 

Model 파일은 위와 같으며, Entity 객체는 Product 테이블 1개만 생성하였다.

그리고 요청 받은거에 대하여 DTO로 리턴해주기 위해 Read와 Create 요청에 대한 함수를 작성하였다.

DTO에 대한 설정은 DTO 파일 작성 부분에서 좀 더 자세하게 진행하려고 한다,

 

추가적으로 Enum을 사용하여 category에 대한 항목을 지정하였다.

굳이 사용을 안해도 서비스에는 문제가 없지만, Enum을 사용함으로서 해당 값 이외에는 허용하지 않으며, 연관된 값들을 그룹으로 관리할 수 있어서 매우 유용하다. 

옵션은 디폴트로 사용하면 숫자 값으로 매핑되어 디비에 저장되기 때문에 String으로 변경을 해줘야 한다.

 

 

 

DTO


package com.resteaxm.product

import java.time.OffsetDateTime

data class ReadProductDTO (
    val id: Long? = null,
    val name: String,
    val category: Category
)

data class CreateProductDTO (
    val name: String,
    val category: Category
) {
    fun toEntity(): Product {
        return Product(
            name = name,
            category = category
        )
    }
}

 

DTO 파일은 위와 같다.

Product에 관한 DTO 파일을 두 개로 (Create, Read)로 나누었다, 왜냐하면 각각의 요청마다 필요한 테이블의 컬럼이 다르기 때문에 이와 같이 나누었다, 

물론 DTO 하나를 사용하여 모두 nullable로 지정할 순 있지만, 코드 중복을 감안하고 이런식으로 작성하였다. 

두 방법 중 정답은 없는 것 같지만 테이블 수가 많을 시 중복되는 코드가 적어 후자가 더 나은 것 같긴하다. 

 

 

 

Repository


package com.resteaxm.product

import org.springframework.data.repository.CrudRepository

interface ProductRepository: CrudRepository<Product, Long> {
    fun findAllBy(): List<Product>
}

 

Repository 파일은 위와 같다.

CrudRepository를 상속 받으면 기본적인 CRUD는 제공이 되기 때문에 간단한 Save 같은 것은 작성할 필요 없다.

 

예시로 get 요청에 대하여 DB에 요청하기 위해 findAllBy() 메서드만 정의하였다.

 

 

 

Service


package com.resteaxm.product.service

import com.resteaxm.product.CreateProductDTO
import com.resteaxm.product.ProductRepository
import com.resteaxm.product.ReadProductDTO
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional


@Component
class ProductService {

    @Autowired
    lateinit var productRepository: ProductRepository

    fun getProducts(): List<ReadProductDTO> {
        val product = productRepository.findAll()
        return product.map { it.toReadProductDTO() }
    }

    @Transactional
    fun createProduct(createProductDTO: CreateProductDTO): CreateProductDTO {
        val product = productRepository.save(createProductDTO.toEntity())
        return product.toCreateProductDTO()
    }
}

 

Service 코드는 Read, Create에 대한 요청만 작성하였으며, DTO 타입으로 전달 받은 요청을 DB에 요청 후 다시 DTO 타입으로 리턴하도록 작성하였다.

 

 

 

Controller


package com.resteaxm.product.controller

import com.resteaxm.product.CreateProductDTO
import com.resteaxm.product.service.ProductService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
class ProductController {

    @Autowired
    private lateinit var productService: ProductService

    @GetMapping("/products", produces = ["application/json"])
    fun getProducts(): ResponseEntity<Any> {
        return ResponseEntity
            .ok()
            .body(productService.getProducts())
    }

    @PostMapping("/product")
    fun createProduct(@RequestBody createProductDTO: CreateProductDTO): ResponseEntity<Any> {
        productService.createProduct(createProductDTO)
        return ResponseEntity
            .ok()
            .body(true)
    }
}

 

마지막으로 Controller 코드는 위와 같다.

GetMapping, PostMapping으로 엔드 포인트를 정의하였으며, 각 요청에 대하여 어떤 service의 메서드를 호출 하는지에 대하여 정의하였다.

 

 

 

실행 결과


실행 후 httpie를 사용하여 Request 결과 아래와 같이 정상 적으로 동작하는 것을 알 수 있다. 

 

 

1. post 요청

 

 

 

2. get 요청

 

 

 

 

Comments