brunch

You can make anything
by writing

C.S.Lewis

spring-data-r2dbc

안녕하세요. Calvin입니다. 

spring 진영에서 가장 핫한 R2DBC의 spring-data 프로젝트인 spring-data-r2dbc에 대해서 살펴볼까 합니다.


R2DBC란 무엇인가?


공식 사이트는 https://r2dbc.io/입니다.


간단하게 정의하면

R2DBC는 Reactive Relational Database Connectivity의 약어로써 적은 수의 스레드로 동시성을 처리하고 더 적은 하드웨어 리소스로 확장할 수 있는 non-blocking 애플리케이션 스택입니다.


spring mvc + jpa가 한쌍이라면, spring webflux + r2dbc가 찰떡궁합입니다. 


어떻게 공부할 건인가?


- spring example: https://github.com/spring-projects/spring-data-examples/tree/master/r2dbc/example

- 하시스 study: https://github.com/kakaohairshop/spring-r2dbc-study

spring-r2dbc-study를 설명하자면


1. gradle dependency 추가

implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
runtimeOnly("com.h2database:h2")
runtimeOnly("io.r2dbc:r2dbc-h2")

만약 mysql이라면 아래와 같이 추가하면 됩니다.

implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
runtimeOnly("mysql:mysql-connector-java")
runtimeOnly("dev.miku:r2dbc-mysql")


2. DatabaseClient 예제

DatabaseClient는 sql을 직접 입력하는 pure client입니다. https://github.com/kakaohairshop/spring-r2dbc-study/blob/main/src/main/kotlin/kr/co/hasys/springr2dbcstudy/shop/ShopControllerV1.kt를 보시면 그 쓰임새를 쉽게 알 수 있을 거예요.


3. R2dbcEntityTemplate 예제

R2dbcEntityTemplate는 spring-data-r2dbc에서 제공하는 Template 클래스입니다. DatabaseClient의 경량 Wrapper라고 생각하면 됩니다. CRUD 시에 요청을 Java 클래스로 전달할 수 있으며 결과도 Java 클래스로 쉽게 받을 수 있습니다. 


https://github.com/kakaohairshop/spring-r2dbc-study/blob/main/src/main/kotlin/kr/co/hasys/springr2dbcstudy/shop/ShopControllerV2.kt에 CRUD 예제를 작성해 두었습니다.


4. R2dbcRepository 예제

이번 포스트에서 주로 살펴볼 spring-data-r2dbc입니다. https://github.com/kakaohairshop/spring-r2dbc-study/blob/main/src/main/kotlin/kr/co/hasys/springr2dbcstudy/shop/ShopControllerV3.kt에서 예제 소스를 확인할 수 있습니다.


R2dbcRepository 자세히


R2dbcRepository의 공식 가이드 문서는 https://docs.spring.io/spring-data/r2dbc/docs/current/reference/html/입니다. spring-data-jpa의 JpaRepository를 사용해 본 경험이 있다면 R2dbcRepository도 쉽게 이해할 수 있습니다. 


조회

조회는 findAll(), findById(), findBy필드명() 등으로 데이터를 가져올 수 있습니다. 단, Reactive Model이기 때문에 List <T> 대신에 Flux <T>를 리턴하고, <T> 대신에 Mono <T>를 리턴합니다. Flux와 Mono에 대한 자세한 내용은 https://www.baeldung.com/reactor-core를 참고하세요.


spring-r2dbc-study에서 살펴본 바와 같이 조회 후 원하는 클래스로 변환하는 방법은 아래와 같습니다.

return shopRepository.findAll()
        . map { ShopResponse(it.id, it.name) }

findAll()로 받은 Flux <Shop>을 map() 메쏘드로 변환해서 return 합니다.


Flux나 Mono나 엘리먼트가 0 일 수 있기 때문에 switchIfEmpty()를 통해서 Exception 처리를 하는 것이 좋습니다.

return shopRepository.findById(id)
        . switchIfEmpty { throw ShopNotFoundException() }


복잡한 연산은  @Query 어노테이션을 통해 처리할 수 있습니다. 아래 소스코드는 lastname으로 찾는 예제입니다.

@Query("SELECT * FROM person WHERE lastname = :lastname")   
fun findByLastname(lastname String): Flux<Person>  


입력

입력은 간단합니다.

shopRepository.save(shop)

그런데 webflux 코드는 이렇게 return 없이 마치면 안됩니다.

return shopRepository.save(shop)

반드시 Mono<T> 로 return 해야 합니다. 그렇지 않으면 실행이 되지 않습니다. 


수정

수정 후 조회 후 flatMap()으로 펼친 후 save() 하는 형태로 처리합니다.

return shopRepository.findById(id)
        .flatMap {
            it.name = request.name
            shopRepository.save(it)
        }

수정 역시 어노테이션으로 가능합니다. 아래 소스코드는 lastname으로 찾아서 firstname을 업데이트하는 예제입니다. 

@Modifying 
@Query("UPDATE person SET firstname = :firstname WHERE lastname = :lastname") 
fun setFixedFirstnameFor(firstname String, lastname String): Mono<Int>

@Modifying 어노테이션은 INSERT, DELETE, UPDATE 시에 추가해 주는 것이 좋습니다. 없어도 동작은 하지만 없으면 리턴이 Mono<Void> 제한됩니다. rowsUpdated 갯수를 리턴 받으려면 @Modifying을 추가해주세요. 


삭제

삭제는 아래와 같습니다.

return shopRepository.deleteById(id)


TIP


1. JPA와 같은 연관관계는 지원이 되지 않습니다. 직원이 매장을 가지고 있는 경우 JPA에서는 styler.getShop(). getId()로 접근이 가능하지만 R2DBC에서는 styler.getShopId()로 접근해야 합니다. 


2. h2 DB의 경우 column이 BIGINT, INT 등이 Java 필드로 매핑이 안되네요. Long, Integer로 매핑되는 것을 기대했는데 매핑이 안됩니다. 그래서 study의 primary key가 부득이 하게 VARCHAR입니다. mysql은 BIGINT가 LONG으로 잘 매핑되니 mysql로 공부하세요.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari