JPA 트랜잭션 R2DBC 트랜잭션 비교해보기 #
최근 프로젝트 구현중 r2dbc에서 트랜잭션처리에 대한 궁금증이 생겼다.
JPA와 R2DBC의 트랜잭션처리 방식을 비교해보려한다.
왜 글을 쓰게 되었나? #
jpa는 블록킹 방식으로 동작하여 해당 메소드가 종료 후 쿼리를 실행한다 따라서 트랜잭션 실행중 데이터베이스에서 에러가 발생해도 호출한 스레드에서 쉽게 처리할수 있다.
반면, r2dbc는 논블럭킹 방식이다보니 이 경우 즉시 쿼리를실행하는데 과연 데이터베이스는 각 쿼리가 어떤 트랜잭션에 속하는지 어떻게 알수있을까?
전통적인 스프링에서의 트랜잭션 처리 with JPA #
트랜잭션 동작과정을 복습하는겸 설명해보려한다.
어플리케이션이 실행되면서 컨텍스트 초기화 과정에서 bean들이 생성될때,
@Transactional이 붙은 메소드가 있는 해당 클래스의 원본클래스를 상속받아 프록시 객체를 생성한다(스프링부트는 CGLIB 프록시를 사용하는걸로 알고있음)
프록시를 생성하는 이유는 메서드의 호출을 가로채어 트랜잭션을 관리하기 위함이라는건 잘 알고있을거라 생각한다
이제 클라이언트에서 @Transactional이 붙은 메서드를 호출하려한다. 이때 실제로는 프록시객체의 메서드가 호출되어 프록시 객체는 메서드의 호출을 가로챈다
트랜잭션 처리 과정:
- 
- invoke(): MethodInvocation을 통해 원본 메소드의 정보를 가져온다.
 
 - 
- invokeWithinTransaction(): 실제 트랜잭션 처리가 수행된다.
 
 - 
- 트랜잭션 정보를 ThreadLocal 에 저장(전파수준, 격리수준,타임아웃 등등).
 
 - 
- 트랜잭션 시작: EntityManager와 영속성 컨텍스트가 생성된다.
 
 - 
- 첫 번째 데이터베이스 액세스 시점: 데이터베이스 커넥션이 확보된다.
 
 - 
- 메소드 실행: 비즈니스 로직이 실행된다.
 
 - 
- 트랜잭션 커밋 시도.
 
 - 
- 영속성 컨텍스트 플러시: 변경사항이 DB에 반영.
 
 - 
- DB 커밋.
 
 - 
- DB 응답 대기.
 
 - 
- 트랜잭션 완료.
 
 - 
- 엔티티매니저 닫힘/영속성 컨텍스트 소멸.
 
 - 
- 메소드 반환.
 
 
트랜잭션 롤백 처리 과정:
- 
- DB 응답 대기.
 
 - 
- DB 에러.
 
 - 
- 트랜잭션 매니저 감지.
 
 - 
- 롤백 프로스레스 시작.
 
 - 
- 데이터베이스에 롤백 명령.
 
 - 
- 영속성 컨텍스트 초기화.
 
 - 
- 메소드 에러 반환.
 
 
내가 생각한 JPA 트랜잭션 동작과정 #
리액티브 스프링에서의 트랜잭션 처리 with R2DBC #
리액티브 스프링에서는 트랜잭션관리는 다르게 동작하지만, 여전히 프록시를 사용한다. 똑같이 어플리케이션 컨텍스트에 빈 초기화시에 해당 클래스를 상속받은 프록시 객체를 생성하고,
메소드 호출시 프록시가 가로챈다는 사실은 똑같다. 
그럼 어떤점이 다를까?
트랜잭션 처리 과정:
- 
- @Transactional 메소드 호출.
 
 - 
- 리액티브 트랜잭션 인터셉터 동작.
 
 - 
- invoke(): ReactiveMethodInvocation을 통해 원본 메소드정보획득
 
 - 
- invokeWithinTransaction(): 실제 트랜잭션 처리 시작.
 
 - 
- 트랜잭션 정보 생성 (전파 수준, 격리 수준, 타임아웃 등).
 
 - 
- 트랜잭션 정보를 Reactor Context에 저장.
 
 - 
- ConnectionFactory를 통해 리액티브 데이터베이스 연결 획득.
 
 - 
- 연결에 BEGIN TRANSACTION 명령 전송
 
 - 
- 메소드 실행: 비즈니스 로직 실행 (Mono 또는 Flux 반환)
 
 - 
- 각 데이터베이스 작업은 획득한 연결을 통해 비동기적으로 실행
 
 - 
- 모든 비즈니스 로직 완료 대기
 
 - 
- 트랜잭션 커밋 시도
 
 - 
- 연결에 COMMIT 명령 전송
 
 - 
- 데이터베이스 응답 대기
 
 - 
- 트랜잭션 완료
 
 - 
- 연결 반환 및 리소스 정리
 
 - 
- 메소드 결과 반환 (Mono 또는 Flux)
 
 
리액티브는 논블럭킹이기에 호출한 스레드로컬 대신 리액티브 컨텍스트를 사용하여 트랜잭션의 정보를 저장한다.
스레드 전환이 일어나도 트랜잭션 시작 시, 트랜잭션 ID나 Connection 정보가 Reactor Context에 저장되어 해당 쿼리가 어떤 트랜잭션에 속하는지 식별할수있다. 
그럼 데이터베이스에서는 해당 쿼리가 어떤 트랜잭션에 속하는지 어떻게 알 수 있단말인가? 즉시 호출을 하는 리액티브에서 커넥션을 여러개 얻는게 아닌 커넥션을 재사용해야한다.
시나리오 #
트랜잭션 시작 (스레드A) → 커넥션 획득.
↓     
쿼리 1 실행 (스레드 A) → 동일 커넥션 사용.
↓ -> 콜백.    
쿼리 2 실행 (스레드 B로 전환) → 여전히 동일 커넥션 사용. 
↓ -> 콜백.        
쿼리 3 실행 (스레드 C로 전환) → 계속 동일 커넥션 사용. 
↓ -> 콜백.    
트랜잭션 종료 (어떤 스레드든) → 커넥션 반환.
만약 쿼리1에서 길어졌을때 쿼리2는 대기해야되는거다. 
물론 스레드는 해당 쿼리를 커넥션에게 위임하고 완료가 되었을시 호출받아 커넥션을 재사용하면 된다는점에서 논블럭킹은 맞다.
트랜잭션은 본질적으로 순차적인 작업을 요하기때문에 트랜잭션의 특성을 지키기위해선 직렬화가 필요한 부분이라 어쩔수없다라는 생각이 든다.
내가 생각한 리액티브 트랜잭션 동작과정 #
JPA 트랜잭션과 R2DBC 트랜잭션 차이점 #
트랜잭션 정보저장
- JPA -> 블럭킹방식이라 호출스레드에 있는 ThreadLocl에 해당 트랜잭션의 정보를 저장한다.
 - R2DBC -> 논블럭킹 방식이라 트랜잭션의 정보를 Reactor Context에 저장 후 스레드 컨텍스트 전환시 해당 트랜잭션의 정보를 저장하고 검색후 쿼리를 호출한다.
 
쿼리 호출과정
- JPA -> 동기방식으로 해당 쿼리를 영속성컨텍스트에서 변경 사항을 저장하고 단일 커넥션으로 쿼리를 한번에 호출한다.(쿼리메소드같은건 즉시 쿼리호출됨)
 - R2DBC -> 비동기방식으로 여러 쿼리를 즉시 호출 후 커넥션을 재사용한다.(커넥션을 재사용하기때문에 callback받은 후 다시 쿼리를 호출해야됨(일관된 작업을위해))
 
트랜잭션 경계
- JPA -> 다양한 트랜잭션 전파기능을 제공한다
 - R2DBC -> NESTED,REQUIRES_NEW(중첩된 트랜잭션)을 제공하지 않음(단 예외처리나,수동으로 관리가능)