업무를 하는 도중, 원자적으로 작업이 되어야만 하는 즉, 트랜잭션이 꼭 필요한 비즈니스 로직이 있었다.
@Transactional 어노테이션을 사용해서 간편하게 트랜잭션 작업을 하고 싶은데.. 여러 블로그에 있는 레퍼런스대로 진행하려 했는데 도통 동작하지 않는 것이었다.
여기에서 작업이 지연되면 앞으로 진행해야 될 업무 일정에 차질이 생길 것 같아 주말에 시간을 내서 이 문제에 대해 꼭 해결을 하고 싶었다.
어디가 문제인지 아예 감이 안잡히는 상황이라 처음부터 하나씩 문제가 될 만한 것들을 수정해가며 소거해볼 예정이다.
사용한 기술
Spring Boot 3.2.x
Coroutine
spring-boot-starter-data-mongodb-reactive
Replica-setting MongoDB
스프링과 MongoDB를 사용할 때 트랜잭션에 있어 주의점이 있다고 한다.
1. Spring 버전 6.1.0 이상, Spring Boot 버전 3.2.0 이상 (이전 버전은 Coroutine AOP를 지원하지 않음)
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes
2. Mongo 버전 4.0 이상( 이전 버전은 트랜잭션 자체를 지원하지 않음)
https://www.mongodb.com/ko-kr/docs/manual/core/transactions/
3. 레플리카 세팅 된 MongoDB
위 주의점들에는 딱히 문제가 없었다.
AI한테 몽고 세팅을 보여주고 트랜잭션이 동작하지 않는 이유를 물어보면, 자꾸 Reactor 문제라고 한다.
그래서 Mono<Void>를 리턴해보기도 했고, 직접 subscribe 하는 방법도 해봤는데.. 당연히도 해결되지 않았다.
정말 이 문제인지 확인해보고 싶어 가장 빠른 방법인, 새로 프로젝트를 만들어 보기로 했다.
첫 번째 작업
개발 환경에 연결된 mongo는 무료 버전이라, 레플리카 세팅이 안 되어 있을 수 있다는 이야기를 들었다.
그래서 로컬 환경에서 레플리카세팅이 제대로 된 db를 하나 구축해서 그걸로 테스트를 해보려 했다.
레플리카 세팅에 필요한 key를 생성하고, docker-compose 등을 설정하고 로컬 환경에서 구동 후, 업무 환경과 버전들이 똑같은 새로운 프로젝트를 하나 만들어 방금 만든 db를 연결해, 거기에서 테스트를 해 보았다.
롤백이 성공적으로 되었다.
DB도 확인을 해 보니 insert되지 않았다.
이 프로젝트는 1개의 db만 연결한 것이고, 공식문서대로 따라한 것이니 잘 될 수밖에 없다.
그렇다면 일단 업무 환경에서 트랜잭션이 동작하지 않는 건 코루틴과 전혀 상관이 없는게 확실해졌다.
두 번째 작업(해결)
레플리카 세팅이 제대로 되어있는 db를 업무 환경의 db 하나와 바꿔치기를 해 보았다. (이게 된다면 업무(개발)환경이 레플리카 세팅이 안된 것)
그리고 트랜잭션 동작 테스트를 해 봤는데, 역시나 롤백이 되지 않았다.
트랜잭션이 시작했다는 것과, 롤백이 동작한다는 로그는 정상적으로 나타났지만 역시나 db에서는 insert가 이미 되어있었다.
브레이크포인트를 잡아, save하는 지점에서 db를 확인해 보면, save를 호출하자마자 db에 insert가 되고 있었다.
업무(개발)환경에서 db가 레플리카 세팅이 제대로 되어있다고 생각하기로 하고, 남은 문제점은 빈 생성을 잘못 했다는것 밖에 없었다.
위와 같이 DatabaseFactory를 생성자로 생성하고, 빈을 생성할 때 넣어주고 있었는데 이 부분이 문제가 있었다.
https://docs.spring.io/spring-data/mongodb/reference/mongodb/client-session-transactions.html
Sessions & Transactions :: Spring Data MongoDB
As of version 3.6, MongoDB supports the concept of sessions. The use of sessions enables MongoDB’s Causal Consistency model, which guarantees running operations in an order that respects their causal relationships. Those are split into ServerSession inst
docs.spring.io
여러 레퍼런스를 참고해본 결과,
스프링과 MongoDB는 동일한 MongoClient 인스턴스와 MongoDatabaseFactory를 공유해야, 트랜잭션 경계 내에서 올바른 세션 전파(session propagation)가 이루어진다고 한다.
그런데 위 코드처럼 기존에 이미 빈으로 등록되어 있는 MongoClient와 연결된 팩토리를 사용하지 않고 SimpleReactiveMongoDatabaseFactory를 생성하는 별도의 MongoClient 인스턴스를 새로 생성하면 애플리케이션 컨텍스트에 등록된 기존 MongoClient 빈과는 다른 인스턴스가 생성된다고 한다.
나는 커넥션만 동일하다면 동작하게 되는줄 알았다.........(실제로 Mysql같은 전통적인 RDB를 쓸때 Client의 인스턴스가 달라도 JDBC 스스로 커넥션의 세션을 자동으로 탐색하고 찾아간다고 한다..)
여러 개의 DB를 연결하게 되어 빈 등록을 직접 하나하나 해주어야 하다보니, 이런 이슈를 마주치게 된 것 같다.
해결 방법은 이미 빈으로 등록된 ReactiveMongoTemplate(또는 그 안의 MongoDatabaseFactory)를 주입받아 사용하거나, 동일한 MongoClient 빈을 재사용하면 된다고 한다.
그러면 빈을 생성하는 코드를 수정해 보자.
기존의 업무 환경에서 트랜잭션을 테스트하던 곳에 적용해보고 실행을 해보면....
트랜잭션 시작, 롤백 로그도 잘 뜨고 DB도 insert되지 않은 걸 확인할 수 있었다.
마치며
빈 생성하는 코드 서너줄이 시간을 많이 날려먹었다.. 처음에는 단순한 설정 문제라고 생각했던 것이, 실제로는 애플리케이션 컨텍스트에 이미 등록되어 있는 빈들을 재사용하지 않은 아주 작은 실수에서 비롯된 것이었기에, 해결하고 나니 한편으로는 안도감과 함께 개발자로서 한 단계 성장했다는 느낌이 들었다.
이 포스트가 비슷한 문제로 고민 중인 다른 개발자들에게 도움이 되기를 바라며, 항상 작은 부분까지 꼼꼼히 확인하는 습관이 얼마나 중요한지 다시 한번 강조하고 싶다.
https://github.com/jonghunju/transactiontest
GitHub - jonghunju/transactiontest: 트랜잭션 테스트.코루틴
트랜잭션 테스트.코루틴. Contribute to jonghunju/transactiontest development by creating an account on GitHub.
github.com
'DEV > 개발일기 || 트러블슈팅' 카테고리의 다른 글
데이터 압축을 위한 Gorilla 알고리즘 적용 사례: 사내 솔루션 개발기 및 회고 (5) | 2024.09.03 |
---|---|
JPA에서 대용량 데이터를 읽거나 수정/삭제 할때, 쿼리를 어떻게 작성해야 할까? (1) | 2024.06.10 |
공유 자원에서의 동시성 이슈는 어떻게 해결해야 할까? (0) | 2023.12.15 |
pg의 멀티테넌시(스키마) 환경에서 스프링 배치를 어떻게 사용해야 할까? (0) | 2023.12.15 |