Contents

스프링 리액티브 트랜잭션

리액티브 컨텍스트 안에서 트랜잭션은 어떻게 이루어질까?

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

선언적 트랜잭션 관리는 어떻게 이루어지나

트랜잭션 관리는 트랜잭션의 상태를 실행과 연관지어야 한다.

선언적 프로그래밍 방식에서는 전형적으로 ThreadLocal 을 사용해서 이루어지고, 트랜잭션의 상태는 쓰레드에 바운드된다.

트랜잭션을 요하는 코드는 트랜잭션을 발생시킨 컨테이너의 동일한 쓰레드에서 실행된다는 것을 전제로 한다.

반면 리액티브 프로그래밍 모델에서는 이 선언적인 프로그래밍 모델의 기능들을 전부 없애야 한다. 동작들은 전부 다른 쓰레드에서 이뤄질 수 있기 때문이다.

프로세스 간의 커뮤니케이션 영역으로 들어오면 좀 더 명확해지는데 - 더 이상 우리의 코드는 동일한 쓰레드에서 작동하지 않는다. 때문에 더 이상 트랜잭션 관리에 ThreadLocal을 사용할 수 없다.

그렇다면, 동일한 쓰레드가 아닌 서로 다른 쓰레드 간에 트랜잭션 상태를 보장해주려면 어떻게 해야 할까?

쓰레드 간 스위칭은 임의의 시간에 발생할 수 있다. 리액티브 프로그래밍 모델에서 적절할 때에 적절한 쓰레드로 스위칭하는 것은 거의 불가능하다. 연산자 간 결합이 더해져서 스트림의 통합과 최적화가 이루어지면 쓰레드 간 스위칭이 언제 일어날 지 예측하는 것은 더 불가능하다.

때문에 트랜잭션 상태를 TransactionStatus 객체를 통해 전달하는 방식에도 변화가 필요하다.

그런데 가만 생각해보면 기존 스프링에서 ThreadLocal을 사용하는 대표적인 예시가 하나 더 있다. 바로 SecurityContext. 우리는 서블릿 기반 스프링에서 쓰레드에 바운드되도록 유저 컨텍스트를 저장해서 사용해왔었고, 트랜잭션도 그렇게 사용하고 있는 것이다.

마찬가지로 리액티브 프로그래밍에서 쓰레드 간 어떤 값을 공유하기 위해서는 - SecurityContext가 변화한 것처럼 - Subscription 을 사용한다.

리액터의 Context는 스프링이 특정 Subscription에 대해 모든 리소스와 동기화를 조절하며 트랜잭션 상태를 바인드 할 수 있도록 한다. Project Reactor를 사용하는 모든 리액티브 코드는 리액티브한 트랜잭션에도 참여하게 되는 셈이다.

스칼라 값을 반환하고 트랜잭션 디테일에 접근하기 위한 기존의 코드들은 다시 트랜잭션에 참여하기 위해서는 리액티브 타입을 사용해서 다시 짜여져야 한다. 그렇지 않을 경우 내부 컨텍스트에 접근할 수 없기 때문이다.

리액티브 트랜잭션이 이루어지는 방식

스프링 5.2 M2 버전부터는 스프링이 ReactiveTransactionManager를 통해 리액티브 트랜잭션을 관리한다.

ReactiveTransactionManager

ReactiveTransactionManager는 트랜잭션의 리소스를 사용하는 리액티브하고 논블락킹한 통합 트랜잭션 관리의 추상화이다. Publisher 타입을 리턴하고 TranscationalOperator를 사용하는 프로그래밍적인 트랜잭션 관리가 필요한 @Transactional 메서드들에 대해 적용된다.

가장 먼저 구현된 리액티브 트랜잭션 매니저에는 R2DBC (Spring Data R2DBC 1.0 M2), MongoDB (Spring Data MongoDB 2.2 M4)가 있다.

깊게 들어가면..

리액티브 트랜잭션은 선언적 트랜잭션의 어노테이션 기반 코드와 매우 유사하게 생겼다.

  • 하지만 리액티브 리소스 추상화인 DatabaseClient와 작업하는 방식이 변경되었다는 점이 다르다.
  • 모든 트랜잭션 관리는 스프링의 트랜잭션 인터셉터들과 ReactiveTransactionManager에 의해 behind the scene에서 일어난다.

스프링은 적용할 트랜잭션 관리를 크게 두 부류로 관리한다.

  1. Publisher 타입을 리턴하는 메서드들 : 리액티브 트랜잭션 관리의 대상
  2. 다른 타입을 리턴하는 메서드들 : 선언적 트랜잭션 관리의 대상

리액티브 트랜잭션을 사용하는 동시에 선언적 컴포넌트인 JPA나 JDBC 쿼리를 작성할 수 있기 때문에 이 차이는 중요하다. 선언적 컴포넌트의 결과물들을 Publisher 타입으로 래핑하는 것은 스프링에게 선언적 트랜잭션 관리를 적용하기 보다는 리액티브 트랜잭션을 적용하도록 알려준다.

즉, 리액티브 트랜잭션 사용은 JPA나 JDBC에서 사용하는 ThreadLocal에 기반한 트랜잭션을 사용하지 않는다.

TransactionalOperator

ConnectionFactory factory = 
ReactiveTransactionManager tm = new R2dbcTransactionManager(factory);
DatabaseClient db = DatabaseClient.create(factory);

TransactionalOperator rxtx = TransactionalOperator.create(tm); 

Mono<Void> atomicOperation = db.execute()
	.sql("INSERT INTO person (name, age) VALUES('joe', 'Joe')")
	.fetch().rowsUpdated()
	.then(db.execute()
		.sql("INSERT INTO contacts (name) VALUES('Joe')")
		.then())
	.as(rxtx::transactional);

위 코드에서 살펴봐야 할 컴포넌트들은 다음과 같다.

  • R2dbcTransactionManager : R2DBC ConnectionFactory를 위한 리액티브 트랜잭션 매니저이다.
  • DatabaseClient : R2DBC 드라이버를 사용하는, SQL 데이터베이스로 접근하는 클라이언트이다.
  • TranscationalOperator : 이 연산자는 모든 upstream R2DBC publisher들과 트랜잭션 컨텍스트를 연관짓는다. 다른 연산자 스타일 ((...::transactional)) 처럼 쓰거나 execute(txStatus -> ...) 처럼 콜백 스타일로 쓸 수 있다.

리액티브 트랜잭션들은 구독 관계 형성에 따라 lazy하게 시작된다. 이 연산자가 트랜잭션을 시작하면,

  • 적절한 격리 수준을 설정하고
  • 데이터베이스 커넥션과 subscriber의 컨텍스트를 연결한다.
  • 트랜잭션에 참여하는 모든 Publisher 인스턴스는 동일한 단일 컨텍스트 바운드 - 트랜잭션 커넥션을 사용한다.

리액티브 - 함수형 연선자 체이닝은 선형적일수도 (단일 Publisher 사용), 비선형적일수도 (여러 스트림을 병합하는 경우) 있다. 리액티브 트랜잭션은 어떤 연산자 스타일을 사용하더라도 모든 업스트림 Publisher들에게 영향을 미친다.

트랜잭션 스코프를 특정 publisher들에게만 제한하기 위해 콜백 스타일을 적용하기 위해서는 아래와 같이 사용할 수 있다.

TransactionalOperator rxtx = TransactionalOperator.create(tm);
Mono<Void> outsideTransaction = db.execute()
	.sql("INSERT INTO person (name, age) VALUES('Jack', 31)")
	.then();
Mono<Void> insideTransaction = rxtx.execute(txStatus -> {
	return db.execute()
		.sql("INSERT INTO person (name, age) VALUES('Joe', 34)")
		.fetch().rowsUpdated()
		.then(db.execute()
			.sql("INSERT INTO contacts (name) VALUES('Joe Black')") 
			.then());
	}).then();
		
Mono<Void> completion = outsideTransaction.then(insideTransaction);

상기 예시에서는 트랜잭션 관리가 execute() 내부의 Publisher 인스턴스들에게만 적용되었다. 다르게 말하면 트랜잭션 자체가 scoped 되었다고 할 수 있다. execute() 내부의 Publisher 인스턴스들은 트랜잭션에 참여하고, outsideTransaction이라고 명명된 Publisher들은 트랜잭션 바깥에서 기능한다. (트랜잭션 적용 X)

R2DBC는 스프링의 리액티브 트랜잭션 인테그레이션 중 하나이다. Spring Data MongoDB를 사용하더라도 멀티 document 트랜잭션을 리액티브 프로그래밍을 사용하는 동시에 쓸 수 있다.

  • Spring Data MongoDB는 ReactiveMongoTransactionManagerReactiveTransactionManager의 구현체로 사용한다.
    • ReactiveTransactionManager는 세션을 생성한다. (MongoDB의 트랜잭션은 세션 단위로 이뤄진다)
    • 관리되는 트랜잭션 내부에서 실행되는 코드가 다중 document 트랜잭션에 참여하도록 세션을 관리한다.