그럴 땐 병렬 요청을 보내고 있는게 아닌지 확인해보세요!
똑같은 증상을 겪었던 누군가의 이슈: https://github.com/spring-projects/spring-data-mongodb/issues/4804
스프링 웹플럭스 환경, @Transactional 을 사용한 구간에서 간헐적으로 NoSuchTransaction 이 발생했다.
springframework.data.mongodb.TransientClientSessionException: Command failed with error 251 (NoSuchTransaction): 'cannot continue txnId -1 for session ... with txnId 1' on server ...
MongoDB 클라이언트 드라이버가 하나의 세션 내에서 트랜잭션 시작 후, 여러 연산을 동시에 요청하는 것을 허용하지 않기 때문
기존 구현: @Transactional 이 붙어있는 함수 내부에서 Flux.flatMap() 을 사용하여 몽고DB로 여러 개 요청을 보내고 있었다.
여러 요청을 보내더라도 몽고DB client 는 단일 세션만 사용하는데..: 이것이 ClientSession 객체다.
ClientSession 객체 안에는 트랜잭션 아이디, 트랜잭션 시작 여부 등을 관리하기 위한 필드와 메소드가 있고 이들은 thread-safe 하지 않다.!!!
ClientSession instances are not thread safe or fork safe. They can only be used by one thread or process at a time.
Drivers MUST document the thread-safety and fork-safety limitations of sessions. Drivers MUST NOT attempt to detect simultaneous use by multiple threads or processes (see Q&A for the rationale).
이는 버그나 특정 버전에 한정된 것이 아니라 디자인 상 의도된 것이다.
커밋되지 않은 쓰기를 사용할 때
세션 ID는 동시에 두 개 이상의 작업에서 사용되어서는 안 됩니다. 드라이버는 승인되지 않은 쓰기 작업에 대한 응답을 기다리지 않으므로, 세션 ID를 언제 재사용할 수 있는지 알 수 없습니다. 이론적으로 드라이버는 커밋되지 않은 쓰기 작업마다 새로운 세션 ID를 사용할 수 있지만, 그렇게 하면 서버에 사용되지 않는 세션이 많이 쌓이게 됩니다.
따라서 드라이버는 어떠한 경우에도 확인되지 않은 쓰기 작업과 함께 세션 ID를 전송해서는 안 됩니다.
그렇다면 flatMap을 썼는데 여러 스레드에서 동일한 1개 세션을 사용하게 된 이유는 뭘까? flatMap 은 여러 요청을 병렬로 보내는거잖아?
라고 생각해서 더 물어보니까 이렇게 알려줬다.
@Transactional 메서드가 호출되면 Spring이 doBegin()을 실행합니다.
즉, 이런 일이 생겼던 것…
@Transactional 붙은 메소드 호출
--> doBegin() 에서 클라이언트 세션 하나 생성
--> Reactor context 에 저장 --> 이후 flatMap 처리하는 각 스레드에서 동일한 context 를 사용 == 동일한 클라이언트 세션 사용 == 1개 클라이언트 세션으로 트랜잭션 요청이 여러개 나감
--> Race condition 발생
flatMap 대신 순차적으로 실행되는 concatMap 을 사용하자. concatMap 함수는 아래와 같이 여러 요소가 있을 때 이전 요소의 처리가 완료된 후 다음 요소를 subscribe 한다. 따라서 @Transactional 이 붙은 메소드 안의 flatMap 을 concatMap 으로 바꾸면 하나의 클라이언트 세션에서 동시에 여러 요청을 보내지 않고 1개씩 순서대로 보내게 되고, 에러가 발생하지 않게 된다.

flatMapSequential : 결과가 순차적이라는 뜻이지 과정도 순차적이라고는 하지 않았다..^^ 이름에 속기 쉬운 함수다. 이 함수의 다이어그램을 확인해보자.

초록색과 분홍색 처리에 주목하자. 내부적으로는 분홍색 네모(결과)가 먼저 나왔지만 input이 들어온 순서는 초록색이 먼저이기 때문에 output 을 낼 때 초록색을 먼저 내보낸다. 그렇다는 건 내부 처리 방식은 비순차적이고 결과 집계만 순서대로 해서 보여준다는 뜻이다. 초록 input 의 종료도 기다리지 않고 분홍 input을 subscribe 한 걸 봐도 알 수 있다. 이번 글에서 생긴 버그는 클라이언트 세션 1개에서 트랜잭션 요청이 여러개 나가서 생긴 것이었기 때문에 이 함수로 바꾸더라도 결국 flatMap 사용했을 때와 같은 에러를 받게 될 것이다.