ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring data R2DBC
    🌱 spring 2025. 4. 13. 13:13

    이번에 진행하는 프로젝트가 Kotlin + Coroutine을 이용함에 따라 DB 통신 기술을 비동기로 사용하도록 해야 했습니다.

    spring-data-jpa를 사용하지 못하기에 database와 비동기로 통신할 수 있는 기술에 대해서 찾아보게 되었습니다.

    📌 Hibernate Reactive

    Hibernate Reactive는 JPA의 Reactive 구현체로 JPA 반응형 구현체

    • ORM 기능을 전부 활용 가능!!

    For a given Session object, nested calls to withTransaction() occur within the same shared transaction context. However, notice that the transaction is a resource local transaction only, delegated to the underlying Vert.x database client, and does not span multiple datasources, nor integrate with JPA container-managed transactions.

     

    Hibernate Reactive는 JTA (Java Transaction API) 를 사용하지 않습니다.

     

    ❗ @Transactional 같은 선언적 트랜잭션은 사용할 수 없고 반드시 withTransaction() 등의 명시적 API를 사용해야 합니다.

    하나의 Session 객체 안에서 withTransaction()을 중첩해서 호출하면 같은 트랜잭션 컨텍스트 안에서 동작

     

    📌 Spring R2dbc

    R2DBC(Reactive Relational Database Connectivity)는 관계형 데이터베이스에 비동기적으로 접근하기 위한 스펙입니다.

     

    JDBC는 블로킹 I/O 기반으로 동작하여 요청을 보내면 응답이 올 때까지 스레드가 기다리는 구조입니다.

    반면에 R2DBC는 비동기 논블로킹 I/O를 기반으로 동작하여 데이터베이스 작업을 처리하는 동안 스레드가 유휴 상태가 되는 것을 방지하고 시스템 리소스를 더 효율적으로 사용할 수 있습니다.

     

    Spring R2DBC는 이러한 R2DBC 스펙을 Spring 생태계에 통합한 솔루션으로 리액티브 프로그래밍 방식으로 데이터베이스와의 통신을 처리합니다.

     

    ❗R2DBC는 ORM이 아닙니다!

    • jakarata.persistence 에서 제공하는 어노테이션들을 사용할 수 없으며 org.springframework.data 하위의 어노테이션들을 이용해서 매핑해줘야합니다.

     

    또한, R2DBC는 객체를 자동으로 만들어주지 않아 직접 테이블을 설계해주어야 합니다.

    • schema.sql을 만들어서 사용하거나 flyway를 사용

     

    📍 Persistable

    Spring data R2DBC를 사용할때 새로운 객체를 저장하거나 업데이트 하기위해서 save메소드를 사용합니다.

    • Spring data R2DBC는 EntityManager가 없고 따라서, 변경감지도 없습니다.

     

    따라서, R2DBC는 @Id와 @Version에 따라서 새로운 값인지 판별하여 Id와 Version을 함께 사용하는 경우에는 Version이 0 이면 새로운 값으로 인식하며 id가 null일때도 새로운 값으로 인식합니다.

     

    새로운 값으로 판별될때만 insert 쿼리를 수행하고 새로운 값이 아니라 기존에 저장된 값이라고 판별될 시 update 쿼리를 수행합니다.

     

    🆚 Hibernate Reactive / R2dbc

     

    항목 Hibernate Reactive R2DBC (with Spring Data R2DBC)
    지원 환경 Spring과 통합이 어려움 Spring 공식 지원Kotlin Coroutine 완벽 호환
    트랜잭션 Resource-local only(JTA, @Transactional 불가) R2DBC 전용 트랜잭션 매니저 지원Spring의 @Transactional 지원 가능 (단, 제한 있음)
    코루틴 호환성 Kotlin Coroutine 직접 지원 안함→ Reactor 또는 Mutiny 기반 ✅ Kotlin Coroutine 친화적 (suspend 함수 기반 API)
    ORM 기능 Hibernate ORM 기반 → 복잡한 도메인 설계, Lazy loading, JPQL 가능 ORM 지원 거의 없음→ JPA-style Entity 매핑은 어려움

     

    현재 진행하고 있는 프로젝트는 Kotlin + Coroutine 기반으로 코드를 작성하고 있습니다.

    따라서, 두 가지 방법을 이용해서 코드를 작성해보았을 때의 예시를 보면 아래와 같습니다.

     

    R2DBC 이용 코드

    suspend fun findUserById(id: Long): User {
        return userRepository.findById(id)
    }
    

     

    Hibernate Reactive는 Mono 또는 Uni를 반환하므로, 직접 코루틴과 연결할 수 없습니다.

    suspend fun findUserById(id: Long): User {
        return sessionFactory.withSession { session ->
            session.find(User::class.java, id)
        }.convert().await()
    }
    

     

    이렇게 Hibernate Reactive은 Kotlin Coroutine 환경에서는 부자연스럽고 비효율적이라 생각해 R2DBC를 사용하기로 했습니다.

     

    🔗 Spring data R2dbc @Transactional

     

    spring data r2dbc를 사용하면서 다음과 같은 의문이 들었습니다.

     

    mvc + spring data jpa 환경에서 선언적 트랜잭션을 사용하면 다음과 같은 방식으로 트랜잭션을 진행합니다.

    1. PlatformTransactionManager가 트랜잭션을 시작.
      • Jpa를 사용한다면 JpaTransactionManager가 PlatformTransactionManager의 구현채로 등록
    2. TransactionSynchronizationManager가 ThreadLocal방식으로 Connection을 유지하고 이후에 같은 스레드에서 요청되는 로직은 ThreadLocal에서 커넥션을 가져와서 사용하게 됩니다.

     

    하지만, ThreadLocal을 사용하지 못하는 상황에서 r2dbc는 어떻게 커넥션을 유지하는 건지 궁금해졌습니다.

     

    🏃 동작 과정

    R2dbcTransactionManagerAutoConfiguration

     

    Spring Boot가 실행이 되면 R2dbcTransactionManagerAutoConfiguration에 의해서 R2dbcTransactionManager가 Bean으로 등록됩니다.

     

    ⬆️ R2dbcTransactionManger 상속

    public interface TransactionManager { }
    
    public interface ReactiveTransactionManager implements TransactionManager ... {}
    
    public class AbstractReactiveTransactionManager implements ReactiveTransactionManager ... {}
    
    public class R2dbcTransactionManager implements AbstractReactiveTransactionManager ... {}

     

    💡 중요한건 R2dbc가 어떻게 db 커넥션을 유지하면서 트랜잭션을 진행하는지!!

    • 트랜잭션이 실행되는 과정을 하나씩 찾아가보면서 확인해보려고 합니다.

     

    1️⃣ R2dbcTransactionManager

    R2dbcTransactionManager에서 사용하는 ConnectionFactory는 상속받은 connectionFactory를 사용

    @Nullable
    public ConnectionFactory getConnectionFactory() {
        return this.connectionFactory;
    }
    

     

     

    2️⃣ AbstractReactiveTransactionManager

    AbstractReactiveTransactionManager - getReactiveTransaction()

     

    • 49 라인: Transaction의 PROPAGATION 정보를 가져온다.
    • 50 라인: TransactionSynchronizationManager로 부터 현재 사용중인 트랜잭션을 가져온다.

     

    org.springframework.transaction.TransactionDefinition

    public interface TransactionDefinition {
        int PROPAGATION_REQUIRED = 0;
        int PROPAGATION_SUPPORTS = 1;
        int PROPAGATION_MANDATORY = 2;
        int PROPAGATION_REQUIRES_NEW = 3;
        int PROPAGATION_NOT_SUPPORTED = 4;
        int PROPAGATION_NEVER = 5;
        int PROPAGATION_NESTED = 6;
        int ISOLATION_DEFAULT = -1;
        int ISOLATION_READ_UNCOMMITTED = 1;
        int ISOLATION_READ_COMMITTED = 2;
        int ISOLATION_REPEATABLE_READ = 4;
        int ISOLATION_SERIALIZABLE = 8;
        int TIMEOUT_DEFAULT = -1;
    

     

     

    3️⃣ TransactionSynchronizationManager

    TransactionSynchronizationManager.forCurrentTransaction() 메서드는 새로운 TransactionSynchronizationManager을 반환

    • 이 과정에서 TransactionContextManager.currentContext() 메서드 사용

     

    4️⃣ TransactionContextManager

    • 리액티브 컨텍스트에 존재하는 트랜잭션 정보를 조회하는 메서드

    TransactionContext가 Reactor context에 포함되어있는지 확인하고 없다면 TransactionContextHolder가 있는지 확인합니다. 만약, 없다면 NoTransactionInContextExcepion을 던집니다.

     

    ⭐️ 이렇게 R2dbc에서 커넥션을 유지하기 위해서 Reactor의 Context를 이용한다는 것을 알 수 있습니다.

     

    Reactor의 Context

    • 리액터의 Context는 쓰레드 로컬과 비슷하게 특정 리액티브 체인 내에서 전역적인 정보를 공유할 수 있도록 도와줍니다.

     

    Mono.deferContextual

    컨텍스트를 지연 평가(lazy evaluation) 하면서 접근

    • Context는 체인 내에서 언제든지 변할 수 있기 때문에 실제 구독 시점에 현재 컨텍스트를 조회해야 안전합니다.
    • deferContextual은 구독 시점에 Context를 조회하기 때문에 최신 상태를 보장

     

    ✅ 이는 이미 있는 트랜잭션을 받아 사용하는 과정이였습니다.

     

    🤔 그럼 새로운 트랜잭션이 생성되는 과정은??

     

     

    Aspect의 진입점을 확인해보면 다음과 같습니다.

     

    createTransactionIfNecessary()를 통해 트랜잭션이 없을 경우 생성해주고

    contextWrite를 통해서 context에 커넥션을 등록해줍니다.

     

     

    📚 참고

    https://www.baeldung.com/spring-hibernate-reactive

    https://hibernate.org/reactive/documentation/1.0/reference/html_single/#_transactions

    https://www.youtube.com/watch?v=dl9dfwBsWes&ab_channel=라인개발실록

    https://engineering.linecorp.com/ko/blog/kotlinjdsl-reactive-criteria-api-with-kotlin

    https://spring.io/projects/spring-data-r2dbc

    https://medium.com/@develxb/r2dbc-with-mysql-771313374b63

    https://medium.com/@develxb/spring-data-r2dbc-커넥션-유지-방법-fb1bc8d83a4f

    https://spring.io/blog/2019/05/16/reactive-transactions-with-spring

    https://medium.com/@AlexanderObregon/how-spring-boot-handles-reactive-transactions-8e8b3cbae8fc

    '🌱 spring' 카테고리의 다른 글

    Transactional Outbox Pattern  (0) 2024.04.23
    @TransactionalEventListener 적용해보기 😎  (0) 2024.03.17
    🍀 MongoDB - QueryDsl with Spring  (0) 2023.05.30
    🐱 Tomcat  (0) 2023.03.12
    ThreadLocal  (0) 2023.01.18

    댓글

Designed by Tistory.