[수미수의 개발 브로구]

[WebFlux] Spring WebFlux R2DBC를 이용한 데이터베이스 연동 본문

Language & Framework/WebFlux

[WebFlux] Spring WebFlux R2DBC를 이용한 데이터베이스 연동

수미수 2023. 8. 14. 00:01
반응형

들어가기 전

  Spring WebFlux#4 까지 비동기 기반의 Spring Framework 인 WebFlux 를 이용하여, 프로젝트를 생성 하고, 비지니스 로직을 구현하기 위한 패키지 구성 그리고 샘플 API 를 생성하여 응답 테스트 까지 하였다. 이번 장에서는 WebFlux 에서 데이터베이스 연동 및 데이터베이스로 부터 데이터를 가공 하여 응답 하는 예제 샘플을 통해서 WebFlux 에서는 어떻게 데이터베이스 연결 하는지에 대해서 알아 본다. 

  WebFlux 에서 데이터베이스와 연동하기 위해서 지금까지 사용했던 JDBC 가 아닌 새로운 프레임워크인 R2DBC (Reactive Relational Database Connectivity) 를 사용하야 하며, 이는 JDBC 와 다르게 Reactive Stream 을 사용하여 블로킹 하지 않으면서 비동기적으로 데이터베이스 연산을 수행 할 수 있게 해준다. 주로 데이터 베이스 연동을 위한 비동기 API 로 사용되며, 대부분의 관계형 데이터베이스 (예, PostgreSQL, MySQL, H2 등)과 통합하여 사용 할 수 있다. WebFlux 와 R2DBC 를 함께 사용하여, WebFlux 의 장점을 살릴 수 있으며, 효율적인 비동기 통신을 할 수 있다. 이를 통해 WebFlux 가 추구 하는 적은 리소스를 사용하면서 대용량 트래픽을 처리 하는 애플리케이션 개발을 할 수 있게된다.

따라하기

  해당 글에서는 고객 도메인에서 고객 테이블에 고객 정보 조회/등록/수정/삭제 라는 Use Case 를 처리 하는 예제를 기반으로 설명 하고자 한다. 

개발 환경

  • Spring WebFlux with 2.5.12 
  • Java 17
  • MySQL
  • Kotlin

build.gradle 설정하기

  R2DBC 를 사용하기 위해서 build.gradle.kt 파일에 dependencies 부분에 아래와 같이 라이브러리 프로젝트를 추가 한다.

............

	implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")

	runtimeOnly("mysql:mysql-connector-java")
	runtimeOnly("dev.miku:r2dbc-mysql")
    
............

테이블 스키마

  아래는  고객 정보를 저장하기 위한 테이블 스키마 SQL 예제이다. 

CREATE TABLE TBL_CUSTOMER(
  ID INT AUTO_INCREMENT COMMENT '아이디',
  CUSTOMER_NAME VARCHAR(14) COMMENT '고객 이름',
  ADDRESS VARCHAR(300) COMMENT '주소',
  ..........................
  PRIMARY KEY (ID)
);

Entity 클래스 생성

  해당 글에서는 앞서 설명한 바와 같이 Customer 도메인과 관련된 서비스를 개발한다고 가정하고, TBL_CUSTOMER 테이블을 조회/처리 하는 비지니스에 대해서 설명 한다. 아래 Customer 엔티티 클래스는 TBL_CUSTOMER 라는 테이블에 대한 속성과 행위를 가지는 클래스이며, 기존에 JPA 를 사용했던 것 처럼 어노테이션을 사용하여, 테이블 명과 Id 값을 지정 해준다.

@Table("TBL_CUSTOMER")
data class Customer(
    @Id
    val id: Long? = null,
    val customerName: String,
    val address: String,
)

Repository 클래스 생성

  데이터 베이스로 부터 데이터를 조회/등록/수정/삭제를 하기 위한 Repository 클래스를 생성 한다. 이때, R2dbEntityTemplate 를 주입하여 사용 한다. 아래 예제 소스는 Customer 엔티티를 이용하여, 새로운 Customer 데이터를 등록, 수정, 조회, 삭제 한다. 이때 리스트 조회 시 Flux 로 반환하고, 단일 조회 시 Mono 로 반환 한다.

R2dbEntityTemplate
R2DBC 에서 제공하는 클래스로, 비동기로 데이터베이스와 상호작용을 해주는 클래스이며, 엔티티 클래스를 통해서 CRUD 작업을 처리할 수 있게 지원 해주는 클래스이다. 해당 클래스를 사용하여 간단하게 CRUD 기능을 사용 할 수 있고, 복잡한 쿼리를 위한 쿼리 메서드 또한 지원 한다.
class CustomerRepository(private val r2dbcEntityTemplate : R2dbcEntityTemplate){

    fun insertCustomer(customer: Customer): Mono<Customer> {
         return r2dbcEntityTemplate.insert(customer)
    }

    fun deleteCustomer(id: Long): Mono<Int> {
        return r2dbcEntityTemplate.delete(Query.query(where("id").`is`(id)), Customer::class.java)
    }

    fun updateCustomer(id: Long, customer: Customer): Mono<Int> {
        return r2dbcEntityTemplate.update(Customer::class.java)
            .matching(Query.query(where("id").`is`(id)))
            .apply(
                Update.update("customerName", customer.customerName)
                    .set("address", customer.address)                  
            )
    }

    fun getCustomerList(): Flux<Customer> {
        return r2dbcEntityTemplate.select(Customer::class.java).all()
    }

    fun getCustomerByCustomerName(customerName: String): Mono<Customer> {
        return r2dbcEntityTemplate.selectOne(Query.query(where("customerName").`is`(customerName)), Customer::class.java)
    }

    fun getCustomerById(id: Long): Mono<Customer> {
        return r2dbcEntityTemplate.selectOne(Query.query(where("id").`is`(id)), Customer::class.java)
    }
}

Service 클래스 생성

  엔티티 클래스와 Repository 클래스를 모두 정의 하였다. 이제는 Customer 라는 도메인에 대해서, 비지니스 Use Case 를 담당하는 Service 클래스를 생성 한다. 아래 예제 소스는 Customer 에 대한 조회, 추가, 수정, 삭제 에대한 Use Case 에 대해서 구현한 클래스 이다.

class CustomerService(private val repository: CustomerRepository) {
    fun getCustomer(id: Long): Mono<Customer> {
        return repository.getCustomerById(id)
            .switchIfEmpty(Mono.defer {
                throw CustomerNotFoundException(CustomerExceptionMessage.CUSTOMER_NOT_FOUND.code, CustomerExceptionMessage.CUSTOMER_NOT_FOUND.message ) })
    }

    fun getCustomerLis(): Flux<Customer> {
        return repository.getCustomerList().switchIfEmpty(Flux.defer { throw CustomerNotFoundException(CustomerExceptionMessage.CUSTOMER_NOT_FOUND.code, CustomerExceptionMessage.CUSTOMER_NOT_FOUND.message ) })
    }

    @Transactional
    fun postCustomer(customer: Customer): Mono<Customer> {
        return repository.insertCustomer(customer)
    }

    @Transactional
    fun putCustomer(id: Long, customer: Customer): Mono<Customer> {
        return repository.getCustomerById(id)
            .switchIfEmpty(Mono.defer { throw CustomerNotFoundException(CustomerExceptionMessage.CUSTOMER_NOT_FOUND.code, CustomerExceptionMessage.CUSTOMER_NOT_FOUND.message ) })
            .then(repository.updateCustomer(id ,customer))
            .then(repository.getCustomerById(id))
    }

    @Transactional
    fun deleteCustomer(id: Long): Mono<Int> {
        return repository.getCustomerById(id)
            .switchIfEmpty(Mono.defer { throw CustomerNotFoundException(CustomerExceptionMessage.CUSTOMER_NOT_FOUND.code, CustomerExceptionMessage.CUSTOMER_NOT_FOUND.message ) })
            .then(repository.deleteCustomer(id))
    }
}

Controller 클래스

결론

  이번 글에서는 WebFlux 를 이용하여 데이터 베이스를 핸들링 하기 위한 방법에 대해서 예제 코드와 함께 설명 하였다. 기본적으로 사용되는 JDBC 의 경우 WebFlux 와 같이 사용 할 경우 오히려 성능면에서 이점을 발휘할 수 없으며, 대안으로 R2DBC 를 사용하여 비동기/논블럭킹 기반으로 데이터베이스를 핸들링 할 수 있다. 실제 업무에 해당 기능을 적용하기 위해 많은 검색을 하였지만 레퍼런스가 부족하였다. 하지만, 현재 업무에서 대용량 요청에도 장애 없이 잘 사용하고 있으며, 복잡한 쿼리가 많이 없는 비지니스 로직에 사용하면 좋을 것 같다.

  

반응형