-
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 환경에서 선언적 트랜잭션을 사용하면 다음과 같은 방식으로 트랜잭션을 진행합니다.
- PlatformTransactionManager가 트랜잭션을 시작.
- Jpa를 사용한다면 JpaTransactionManager가 PlatformTransactionManager의 구현채로 등록
- 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