방구석 CTO
356
2022-05-03 12:19:14
0
216

[리액티브 코프링] R2DBC 사용법 (데이터 조회하기)


안녕하세요. 제가 블로그에 새로운 글을 연재하게 되었습니다.

스프링과 코틀린 그리고 R2DBC을 이용한 비동기 프로그래밍이 주제인데요.

데이터 조회, 데이터 저장과 수정, 연관관계 구현, 코루틴 순으로 연재할 예정입니다.

많은 관심 부탁드립니다.

원본글: https://anomie7.tistory.com/93




이전글 : [리액티브 코프링] R2DBC 사용법 (들어가며)

모든 예제 코드는 필자의 github 레포지토리 에서 확인할 수 있다.

2. 엔티티 선언하기

이번 예제에서 사용할 엔티티들을 선언하자.
Spring Data R2DBC에서는 연관관계를 지원하지 않는다.
객체간 연관관계를 구성하는 멤버에는 @Transient 어노테이션을 사용해줘야 스프링 실행 시 오류가 나지 않는다.


data class Cart(
    @Id
    val id: Long? = null,
    @Transient
    var cartItems: List<CartItem> = listOf()
)
data class CartItem(
    @Id
    val id: Long? = null,
    var quantity: Int = 1,
    @Column("cart_id")
    var cartId: Long? = null,
    @Column("item_id")
    var itemId: Long? = null,
    @Transient
    var item: Item
)
data class Item(
    @Id val id: Long? = null,
    var name: String,
    var price: Double
)



3. 데이터 조회하기

이 예제에서 다루는 데이터를 조회하는 방법은 크게 2가지이다.

  1. repository 사용
    1. 기본으로 제공하는 메서드 사용
    2. @Query 사용해서 native query 사용
  2. dataBaseClient 이용하기

우선 아래처럼 레포지토리를 선언해보도록 하자

@Repository
interface CartRepository : ReactiveCrudRepository<Cart, Long>, CartCustomRepository {
    @Query("select * from cart")
    fun findAllByQuery(): Flux<Cart>
}

interface CartCustomRepository {
    fun getAll(): Flux<Cart>
}

@Repository
class CartCustomRepositoryImpl(val dataBaseClient: DatabaseClient) : CartCustomRepository {
    override fun getAll(): Flux<Cart> {
        return dataBaseClient.sql("""
            SELECT * FROM cart
        """).fetch().all().map {
            val id = it["id"] as Long
            Cart(id)
        }
    }

}


cart 엔티티를 모두 불러오는 로직들을 구현했다.
CartCustomRepositoryImpl는 코드가 장황한데 추후 join을 이용해서 연관관계를 구현하기 위해서 미리 만들었다.

위 레포지토리의 테스트 코드를 작성해서 실행 결과를 확인해보도록 하자.
필자가 작성한 테스트 코드는 아래와 같다.


@SpringBootTest
class CartRepositoryTest {
    @Autowired
    private lateinit var cartRepository: CartRepository

    @Test
    fun getAllTest() {
        cartRepository.getAll()
            .`as`(StepVerifier::create)
            .thenConsumeWhile {
                Assertions.assertNotNull(it)
                true
            }.verifyComplete()
    }

    @Test
    fun findAllByQueryTest() {
        cartRepository.findAllByQuery()
            .`as`(StepVerifier::create)
            .thenConsumeWhile {
                Assertions.assertNotNull(it)
                true
            }.verifyComplete()
    }

    @Test
    fun findAllTest() {
        cartRepository.findAll()
            .`as`(StepVerifier::create)
            .thenConsumeWhile {
                Assertions.assertNotNull(it)
                true
            }.verifyComplete()
    }
}

4. 다이내믹 쿼리(dynamic query)로 데이터 조회하기

개발할 때 다이내믹 쿼리(dynamic query)를 사용해야 하는 경우가 무조건 생기게되는데.
이를 위해 Spring Data R2DBC에서는 Query-by-Example (QBE)라는 기능을 제공한다.
QBE를 사용하기 위해서는 아래 코드와 같이 레포지토리가 ReactiveQueryByExampleExecutor를 구현해야한다.

@Repository
interface ItemRepository : ReactiveCrudRepository<Item, Long>, ReactiveQueryByExampleExecutor<Item>


아래는 QBE를 테스트하는 코드이다.
꼭 직접 실행해서 결과를 확인해보도록 하자


@SpringBootTest
class ItemRepositoryTests {

    @Autowired
    private lateinit var itemRepository: ItemRepository

    @Test
    fun testDynamicQueryByObject() {
        val name = "Alf alarm clock"
        val price = 19.99
        val example = Example.of(Item(name = name, price = price))
        itemRepository.findAll(example)
            .`as`(StepVerifier::create)
            .expectNextMatches {
                Assertions.assertEquals(it.name, name)
                Assertions.assertEquals(it.price, price)
                true
            }
            .verifyComplete()
    }

    @Test
    fun testDynamicQueryByMatcher() {
        val name = "IPhone"
        val price = 0.0
        val matcher = ExampleMatcher.matching()
            .withMatcher("name", contains().ignoreCase())
            .withIgnorePaths("price")
        val example = Example.of(Item(name = name, price = price), matcher)

        itemRepository.findAll(example)
            .`as`(StepVerifier::create)
            .thenConsumeWhile {
                Assertions.assertTrue(it.name.contains(name))
                true
            }
            .verifyComplete()
    }
}


Spring Data에서 제공하는 기능이므로 간편하게 추가 기능 없이 사용할 수 있다는 장점이 있다.
다만, 항상 엔티티 객체를 생성해야 한다는 단점이 있다.
그리고 실무의 복잡한 요구사항에 대응 못하는 상황이 있을 수 있다.
그래서 필자는 dataBaseClient를 이용해서 직접 다이내믹 쿼리를 구성하는 예제를 구성해보았다.


@Repository
class ItemCustomRepositoryImpl(
   private val dataBaseClient: DatabaseClient
) : ItemCustomRepository {
   override fun searchItem(item: Item): Flux<MutableMap<String, Any>> {
      var selectQuery = "SELECT * FROM item "
      val whereClause = mutableListOf<String>()
      if (item.name.isNotEmpty() || item.price != 0.0) {
         if (item.name.isNotEmpty()) {
            whereClause.add("UPPER(item.name) like UPPER('%${item.name}%')")
         }

         if (item.price != 0.0) {
            whereClause.add("item.price = ${item.price}")
         }
         selectQuery += whereClause.joinToString(" AND ", "WHERE ")
      }
      return dataBaseClient.sql(selectQuery).fetch().all()
   }
}

전체 코드 참고

QBE에 비해서 코드가 장황해졌다.
그리고 쿼리 결과를 Map 데이터 타입으로 받기 때문에 원하는 값을 얻기 위해서 매핑 로직을 별도로 구현해야 하는 번거로움이 있다.
그렇다면 위 코드는 어떤 장점이 있을까?

  1. Item 엔티티에 무조건 의존하지 않아도 된다 (위 예제에서는 Item 엔티티를 파라미터로 사용했지만, 사용자 정의 DTO를 사용할 수도 있다.)
  2. 네이티브 쿼리를 직접 작성하기 때문에 개발자의 선택지가 다양해진다. (Spring Data의 스펙에 의존하지 않는다.)

비즈니스 요구사항은 복잡하고 예측할 수 없기 때문에 코드가 장황해지더라도 네이티브 쿼리를 구성하는 것도 나쁜 선택이 아니라고 생각한다.
목적과 의도에 맞게 코드를 구성해서 사용하도록 하자.

아래는 위 코드를 테스트하는 코드이다.
꼭 직접 실행해서 결과를 확인해보도록 하자


@SpringBootTest
class ItemRepositoryTests {

    @Autowired
    private lateinit var itemRepository: ItemRepository


   @Test
   fun testSearchByName() {
      val iphone = "IPHONE"
      val item = Item(name = iphone, price = 0.0)
      itemRepository.searchItem(item)
         .`as`(StepVerifier::create)
         .thenConsumeWhile {
            val name = it["name"] as String
            Assertions.assertTrue(name.uppercase().contains(iphone.uppercase()))
            true
         }
         .verifyComplete()
   }

   @Test
   fun testSearchByPrice() {
      val price = 20.99
      val item = Item(name = "", price = price)
      itemRepository.searchItem(item)
         .`as`(StepVerifier::create)
         .thenConsumeWhile {
            Assertions.assertEquals(it["price"] as Double, price)
            true
         }
         .verifyComplete()
   }

   @Test
   fun testSearchByNameAndPrice() {
      val iphone = "iphone"
      val price = 20.99
      val item = Item(name = iphone, price = price)
      itemRepository.searchItem(item)
         .`as`(StepVerifier::create)
         .thenConsumeWhile {
            val name = it["name"] as String
            Assertions.assertTrue(name.uppercase().contains(iphone.uppercase()))
            Assertions.assertEquals(it["price"] as Double, price)
            true
         }
         .verifyComplete()
   }
}

부록. 추가적인 데이터 조회 방법

필자가 작성한 예제 이외에도 데이터 조회를 위한 방법이 몇 가지 더 있다.
간단하게 알아보도록 하자.

1. 쿼리 메서드

Spring Data JPA를 사용해봤다면 익숙한 기능일 것이다.
아래 코드와 같이 레포지토리에 Spring Data에서 지원하는 규칙에 맞게 키워드를 조합한 함수명으로 쿼리문 작성을 대신할 수 있다.

@Repository
interface ShopRepository : R2dbcRepository<Shop, String> {

    fun findByName(name: String): Flux<Shop>

    fun findFirstByName(name: String): Mono<Shop>
}

코드 출처: 카카오 헤어 git repository

관련 공식 문서 링크

  1. https://docs.spring.io/spring-data/r2dbc/docs/current/reference/html/#repositories.query-methods
  2. https://docs.spring.io/spring-data/r2dbc/docs/current/reference/html/#appendix.query.method.subject


2. Fluent API

Fluent API를 이용하면 IDE의 자동 완성 기능을 사용해서 쿼리를 작성할 수도 있다.
여러 연산을 연결해서 코드를 작성할 수 있다는 장점이 있다.
Fluent API는 join을 지원하지 않아서 필자는 사용하지 않았다.
아래 코드를 보도록 하자.

public class PostRepository {

   private final R2dbcEntityTemplate template;

   public Flux<Post> findByTitleContains(String name) {
      return this.template.select(Post.class)
              .matching(Query.query(where("title").like("%" + name + "%")).limit(10).offset(0))
              .all();
   }

   public Flux<Person> findAll(String name) {
      return Flux<Person> people = template.select(Person.class)
              .all();
   }

   public Flux<Person> findByFirstNameAndLastNameSortDesc(String name) {
      Mono<Person> first = template.select(Person.class)
              .from("other_person")
              .matching(query(where("firstname").is("John")
                      .and("lastname").in("Doe", "White"))
                      .sort(by(desc("id"))))
              .one();
   }
}

관련 공식 문서 링크

  1. https://docs.spring.io/spring-data/r2dbc/docs/current/reference/html/#r2dbc.entityoperations.fluent-api

참고 자료

1
  • 댓글 0

  • 로그인을 하시면 댓글을 등록할 수 있습니다.