비동기·논블로킹 환경에서 중복 요청 방지를 위한 duplicate Key 저장 시, 동기 블로킹 방식의 JDBC 대신 R2DBC를 사용하였습니다.
리액티브 환경에서 관계형 데이터베이스에 접근하기 위한 표준 API
R2DBC는 JDBC와 유사하게 DB 벤더에 독립적인 공통 API를 제공하며, 각 데이터베이스에 대한 R2DBC 드라이버만 존재한다면 코드 변경 없이 사용할 수 있습니다.
단점
R2DBC는 지원하는 기능이 상대적으로 제한적이며, @ManyToOne, @OneToMany와 같은 연관관계 매핑과 다양한 기능을 제공하지 않습니다.
JDBC와의 차이
R2DBC는 JDBC와 달리 쿼리 실행 과정에서 애플리케이션 스레드를 블로킹하지 않고, 결과가 준비되는 대로 비동기적으로 전달하도록 설계되어있습니다.
이로 인해 JDBC처럼 스레드가 대기 상태에 머무르지 않고, 적은 수의 스레드로도 많은 요청을 동시에 처리할 수 있어 높은 확장성을 제공합니다.
리액티브 환경에서의 @Transactional 동작 방식은 기존 MVC 환경과 차이가 존재합니다.
기존의 MVC + JPA 환경에서는 ThreadLocal을 이용해 트랜잭션을 보장하지만, 리액티브 환경에서는 하나의 요청이 여러 스레드를 거치기 때문에 ThreadLocal 기반 정보가 유지될 수 없습니다.
따라서, Reactor Context라는 별도의 객체를 통해 트랜잭션을 보장합니다.
Reactor Context
스레드에 종속되지 않는 key-value 형태의 컨텍스트 저장소
Reactor Context는 각 리액티브 체인의 구독자( Subscriber )마다 독립적으로 유지되며, 해당 체인을 실행하는 스레드는 실행 시점에 이 Context를 참조합니다.
이를 통해 스레드가 고정되지 않는 비동기 환경에서도 트랜잭션을 유지할 수 있습니다.
리액티브 트랜잭션의 주의점은 리액티브 체인이 끊기지 않도록 유지하는 것이며, 블로킹 호출이나 별도 구독을 통해 Context 전파가 중단되지 않도록 해야 합니다.
흐름
트랜잭션이 시작되면 Spring은 R2DBC Connection을 생성하고, 현재 트랜잭션에 참여할 이 Connection을 Reactor Context에 저장합니다.
이후 R2DBC 기반의 데이터베이스 작업이 수행될 때마다, 실행 흐름에 연결된 Reactor Context에서 해당 Connection을 조회하여 사용한다.
이와 같이 트랜잭션에 바인딩된 Connection이 실행 흐름을 따라 전파되므로, 비동기 환경에서도 각 DB 작업은 동일한 Connection을 재사용하게 되므로, 하나의 @Transactional 범위 내에서 실행되는 모든 쿼리는 동일한 Connection 위에서 처리되며, 이를 통해 리액티브 환경에서도 트랜잭션이 보장됩니다.
( 트랜잭션은 메서드 진입 시점이 아니라 체인이 구독되는 시점에 시작됩니다. )
jpa 의존성 제거
// Mysql 의존성
~~implementation("mysql:mysql-connector-java:8.0.33")~~
implementation ("org.springframework.boot:spring-boot-starter-data-r2dbc")
// MySQL 전용 R2DBC 드라이버
implementation("io.asyncer:r2dbc-mysql:1.1.0")
spring.r2dbc.url=r2dbc:mysql://localhost:3306/[DB명]?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul
spring.r2dbc.username=[username]
spring.r2dbc.password=[password]