본문 바로가기

책/코틀린 리액티브 프로그래밍

10. 스프링 JPA와 하이버네이트를 사용한 REST API

스프링 부트, 하이버네이트 JPA를 사용한 REST API

이 프로젝트에서는 JPA를 사용한다. 따라서 JPA 의존성을 추가해야한다.

gradle 에서 의존성 목록을 추가한다.

comple('org.springframework.boot:spring-boot-starter-data-jpa')

application.properties 에는 database 정보를 추가한다.

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url = jdbc:mysql://localhost:3306/tododb
spring.datasource.username = root
spring.datasource.password = password


## Hibernate Properties

# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update

Todo 클래스를 JPA에 맞게 수정했다.

  • @Entity : @Entity로 애너테이션 된 모든 클래스에 데이터베이스의 테이블이 생성된다.
  • @Id : 테이블의 기본키를 정의한다.
  • @GeneratedValue : 필드 값이 자동 생성되야 함을 나타낸다. JPA에는 다음과 같이 ID생성을 위한 세 가지 전략이 있다.
    • GenerationType.TABLE : 고유성을 보장하기 위해 기본 키가 테이블을 사용해 생성돼야 함을 의미한다. 즉 단일 열과 행을 가지는 테이블에 next_val 값을 유지하는데, 대상 테이블(엔티티로 작성된 테이블)에 삽입될 때마다 기본키에 next_val 값을 설정하고 next_val을 증가시킨다.
    • GenerationType.SEQUENCE : 기본키가 기본 데이터베이스로 시퀀스로 생성되야 함을 의미한다. 
    • GenerationType.IDENTITY : 기본 키가 기본 데이터베이스 ID로 생성되야 함을 의미한다.
    • GenerationType.AUTO : 적절한 생성 전략이 자동으로 선택되야 함을 나타낸다.
  • @get: NotBlank : 테이블 필드가 null이 아니어야 함을 의미한다. 
package com.rivuchk.reactivekotlin.todoapplication

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.validation.constraints.NotBlank

@Entity
data class Todo (
        @Id @GeneratedValue(strategy = GenerationType.AUTO)
        var id:Int = 0,

        @get: NotBlank
        var todoDescription:String,

        @get: NotBlank
        var todoTargetDate:String,

        @get: NotBlank
        var status:String
) {
        constructor():this(
                0,"","",""
        )
}

 

  • @Repository : 이 인터페이스가 프로젝트의 DAO 클래스로 사용돼야함을 나타낸다. 
  • 구현된 JpaRepository의 첫 번째 매개변수는 Entity 이고 두번째 매개변수는 ID 필드의 유형이다.
package com.rivuchk.reactivekotlin.todoapplication

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface TodoRepository: JpaRepository<Todo,Int>

 

응답 JSON을 구조화 하기 위해 ResponseModel 이라는 클래스를 생성했다.

package com.rivuchk.reactivekotlin.todoapplication

data class ResponseModel (
        val error_code:String,
        val error_message:String,
        val data:List<Todo> = listOf()
) {
    constructor(error_code: String,error_message: String,todo: Todo)
            :this(error_code,error_message, listOf(todo))
}

 엔드 포인트로 Controller 클래스를 구현했다.

package com.rivuchk.reactivekotlin.todoapplication

import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import javax.validation.Valid

@RestController
@RequestMapping("/api")
class TodoController(private val todoRepository: TodoRepository) {

    @RequestMapping("/get_todo", method = arrayOf(RequestMethod.POST))
    fun getTodos() = ResponseModel("0","", todoRepository.findAll())

    @RequestMapping("/add_todo", method = arrayOf(RequestMethod.POST))
    fun addTodo(@Valid @RequestBody todo:Todo) =
            ResponseEntity.ok().body(ResponseModel("0","",todoRepository.save(todo)))

    @RequestMapping("/edit_todo", method = arrayOf(RequestMethod.POST))
    fun editTodo(@Valid @RequestBody todo:Todo):ResponseModel {
        val optionalTodo = todoRepository.findById(todo.id)
        if(optionalTodo.isPresent) {
            return ResponseModel("0", "Edit Successful",todoRepository.save(todo))
        } else {
            return ResponseModel("1", "Invalid Todo ID" )
        }
    }

    @RequestMapping("/add_todos", method = arrayOf(RequestMethod.POST))
    fun addTodos(@Valid @RequestBody todos:List<Todo>)
            = ResponseEntity.ok().body(ResponseModel("0","",todoRepository.saveAll(todos)))

    @RequestMapping("/delete_todo/{id}", method = arrayOf(RequestMethod.DELETE))
    fun deleteTodo(@PathVariable("id") id:Int):ResponseModel {
        val optionalTodo = todoRepository.findById(id)
        if(optionalTodo.isPresent) {
            todoRepository.delete(optionalTodo.get())
            return ResponseModel("0", "Successfully Deleted")
        } else {
            return ResponseModel("1", "Invalid Todo" )
        }
    }

}

 

리액터를 이용한 리액티브 프로그래밍

ReactiveX 프레임워크와 마찬가지로 리액터도 4세대 반응형 프로그래밍 라이브러이다. ReactiveX와 비교할 때 몇가지 중요한 차이점이 있다. 

  • ReactiveX는 여러 플랫폼 언어를 지원하는 것과 달리 리액트는 JVM만 지원한다.
  • 자바6 이상을 사용하는 경우 RxJava와 RxKotlin을 사용할 수 있다. 그러나 리액터는 자바 8이상이 필요하다.
  • 리액터와 RxKotlin은 리액터가 수행하는 ComputableFuture, Stream, Duration 같은 자바 8 기능 API와의 직접적인 통합을 제공하지 않는다.
  • 안드로이드 반응형 프로그래밍을 구현하려는 경우 RxAndroid, RxJava, 또는 RxKotlin 또는 Vert.X 를 사용해야 하고, 동시에 최소 안드로이 SDK 26 이상을 필요로하며 그마저도 정식 지원은 아닌다. 리액터 프로젝트는 안드로이드에서 공식적으로 지원되지 않으며 안드로이드 SDK 26 이상에서만 작동한다.  

프로젝트에 리액터 추가

gradle에는 다음의존성을 추가한다.

compile 'io.projectreactor:reactor-core:3.1.1.RELEASE'

메이븐에는 pom.xml 파일에 의존성을 추가한다.

<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
    <version>3.1.1.RELEASE</version>
</dependency>

 

플럭스와 모노 이해

리액터에서는 Rx와 마찬가지로 프로듀서와 컨슈머 모듈을 보유하고 있다. 

  • 플럭스(Flux) : Flowable (백프레셔를 지원한다.)
  • 모노(Mono) : Single 과 Maybe의 조합으로 사용한다.

아래 예제는 RxKotlin과 유사한데 유일한 차이점은 Flowable 대신 Flux를 사용한다는 점이다. 

package com.rivuchk.packtpub.reactivekotlin.chapter11

import reactor.core.publisher.Flux
import java.util.function.Consumer

fun main(args: Array<String>) {
    val flux = Flux.just("Item 1","Item 2","Item 3")
    flux.subscribe(object:Consumer<String>{
        override fun accept(item: String) {
            println("Got Next $item")
        }

    })
}

/* 결과
[DEBUG] (main) Using Console logging
Got Next Item 1
Got Next Item 2
Got Next Item 3
*/

 

아래 예제에서 log 연산자는 플럭스 또는 모노의 모든 이벤트의 로그를 얻을 수 있도록 한다. 

  • 주석 1 에서는 모든 구독을 사용할 Consumer 인스턴스를 생성했다.
  • 주석 2 에서는 Mono.empty() 팩토리 메서드로 비어있는 Mono를 생성했다.
  • 주석 3 에서는 Mono.justOrEmpty() 팩토리 메서드로 null을 전달하여 비어있는 Mono를 생성했다.
  • 주석 4 에서는 Mono.justOrEmpty() 팩토리 메서드 전달된 값 "A String"으로 Mono를 생성했다.
  • 주석 5 에서는 toMono 확장함수로 Mono를 생성했다.
package com.rivuchk.packtpub.reactivekotlin.chapter11

import reactor.core.publisher.Mono
import reactor.core.publisher.toMono
import java.util.function.Consumer

fun main(args: Array<String>) {

    val subscriber = object : Consumer<String> {              // 1
        override fun accept(item: String) {
            println("Got $item")
        }
    }


    val emptyMono = Mono.empty<String>()                      // 2
    emptyMono
            .log()
            .subscribe(subscriber)

    val emptyMono2 = Mono.justOrEmpty<String>(null)           // 3
    emptyMono2
            .log()
            .subscribe(subscriber)

    val monoWithData = Mono.justOrEmpty<String>("A String")   // 4
    monoWithData
            .log()
            .subscribe(subscriber)

    val monoByExtension = "Another String".toMono()           // 5
    monoByExtension
            .log()
            .subscribe(subscriber)
}

/* 결과
[DEBUG] (main) Usion Console logging
[ INFO] (main) onSubscribe([Fuseable] Operators.EmptySubscription)
[ INFO] (main) request(unbounded)
[ INFO] (main) onComplete()
[ INFO] (main) onSubscribe([Fuseable] Operators.EmptySubscription)
[ INFO] (main) request(unbounded)
[ INFO] (main) onComplete()
[ INFO] (main) | onSubscribe([Synchronous Fuseable] Operators.ScalarSubscription)
[ INFO] (main) | request(unbounded)
[ INFO] (main) | onNext(A String)
Got A String
[ INFO] (main) | onComplete()
[ INFO] (main) | onSubscribe([Synchronous Fuseable] Operators.ScalarSubscription)
[ INFO] (main) | request(unbounded)
[ INFO] (main) | onNext(A String)
Got Another String
[ INFO] (main) | onComplete()


*/