database
MongoDB는 원래 “단일 문서 쓰기는 원자적으로 처리”하는 특성을 가졌지만, 복잡한 비즈니스 로직이나 여러 문서를 동시에 업데이트해야 하는 상황에서는 이보다 더 강력한 트랜잭션 기능이 필요했습니다. 이에 따라 MongoDB 4.0부터는 Replica Set 환경에서, 그리고 MongoDB 4.2부터는 Sharded Cluster 환경에서도 Multi-Document Transactions를 공식 지원하게 되었는데요. 이번 글에서는 이러한 Multi-Document Transactions의 도입 배경, ACID 보장 범위와 성능 이슈, 그리고 트랜잭션으로 인해 잠금 범위가 확장되면서 생길 수 있는 동시성 저하 문제를 다뤄봅니다.
MongoDB는 처음 등장했을 때부터 “단일 문서의 원자성”을 강점으로 내세웠습니다. 하나의 문서를 업데이트할 때는 RDB처럼 복잡한 트랜잭션 설정 없이도 원자적(Atomic) 처리가 가능하기에, 빠른 쓰기와 높은 확장성을 자랑했죠. 하지만 여러 문서를 동시에 업데이트해야 하는 결제 처리나 재고 관리 같은 복합 비즈니스 로직을 구현할 때는, 단일 문서 원자성만으로 부족한 상황이 발생했습니다. 이런 요구를 충족하기 위해 MongoDB 4.0부터는 Replica Set 환경 기준으로, 그리고 MongoDB 4.2부터는 Sharded Cluster 환경에서도 Multi-Document Transactions가 공식 지원되면서 보다 유연하고 강력한 데이터 일관성을 보장할 수 있게 되었습니다.
MongoDB의 트랜잭션은 전통적인 RDB의 트랜잭션 모델과 흡사합니다. "세션(Session)"을 기반으로 여러 쿼리를 순차적으로 실행하고, 이를 startTransaction으로 시작해 commitTransaction 또는 abortTransaction으로 명시적으로 종료함으로써 한 덩어리의 작업을 하나의 트랜잭션으로 묶는 방식입니다. 이렇게 정의된 트랜잭션 안에서는 여러 문서를 동시에 쓰더라도 모두 성공(Commit)하거나 모두 실패(Abort)하는 원자적 실행을 지원합니다. 결과적으로 복합적인 업데이트 로직에서도 단 한 번의 커밋으로 데이터를 확정할 수 있어, 개발자의 입장에서 데이터 무결성을 보다 쉽게 관리할 수 있게 되었습니다.
MongoDB의 Multi-Document Transactions는 ACID 특성을 상당 부분 구현합니다. 먼저 원자성(Atomicity) 측면에서, 트랜잭션 내 모든 연산이 전부 반영되거나 전부 반영되지 않음이 보장됩니다. 일관성(Consistency)은 RDB처럼 엄격한 스키마와 참조 무결성을 자동으로 지원하진 않지만, 적어도 트랜잭션 범위 내에서는 변경이 불완전하게 남지 않도록 처리됩니다. 독립성(Isolation)은 문서 수준 격리를 바탕으로, 트랜잭션을 사용할 때는 보다 강화된 스냅샷 격리 수준을 근접하게 제공합니다. 마지막으로 지속성(Durability)은 커밋된 변경 사항이 설정한 Write Concern(w: "majority" 등)에 따라 레플리카에 복제되어, 장애 상황에서도 데이터가 유실되지 않도록 설계되었습니다. 다만 RDB의 Repeatable Read나 Serializable과 정확히 동일한 수준의 격리를 기대하기보다는, MongoDB 특유의 문서 모델에 최적화된 형태로 동작한다는 점을 주의해야 합니다.
MongoDB의 멀티 문서 트랜잭션은 매우 편리하지만, 동시에 성능 이슈를 동반할 수 있습니다. 다중 문서를 수정하다 보면 문서 레벨 락뿐 아니라 컬렉션 단위의 메타데이터 락이 길어질 수 있고, 복수의 샤드에 걸쳐 트랜잭션이 이뤄질 땐 잠금 경합이 더욱 심해집니다. 또한 트랜잭션이 길어지면 롤백과 언두 로그를 오래 보관해야 해서 WiredTiger 캐시 사용량과 I/O 부담도 증가합니다. 이런 이유로 MongoDB 공식 문서에서도, 트랜잭션은 “짧고 집중적으로” 사용하길 권장합니다. 되도록 작은 범위 안에서 필요한 작업만 묶어 처리하고, 비핵심적인 업데이트나 읽기는 트랜잭션 바깥에서 진행해 락 경합과 성능 하락을 최소화하는 것이 바람직합니다.
MongoDB는 기본적으로 단일 문서를 업데이트할 때 그 문서에만 배타적(Exclusive) 락이 걸리도록 설계되어 있습니다. 그러나 여러 문서를 한꺼번에 변경해야 하는 멀티 도큐먼트 트랜잭션의 경우, 동시에 여러 문서를 잠가야 하므로 락 범위가 확장되고 병목(Bottleneck)이 발생할 가능성이 높아집니다. 단일 문서 단위로 처리할 때는 크게 부각되지 않았던 락 경합 문제가, 멀티 문서 트랜잭션으로 넘어가면 동시성 측면에서 추가적인 부담이 될 수 있다는 점을 인지해야 합니다.
이러한 병목을 완화하기 위해서는 트랜잭션 범위와 쿼리 구조를 세심하게 관리해야 합니다. 우선 트랜잭션 범위를 축소해 필요한 연산만 단기간에 처리하도록 설계하고, 불필요한 문서 스캔이나 인덱스 없는 필드 기반의 대량 업데이트를 지양해야 합니다. 또한 MongoDB 스키마 디자인을 재검토하여, 원래 RDB 시절에는 여러 테이블(컬렉션)에서 트랜잭션이 필요한 로직도 중첩 문서(Embedded Document)나 Denormalization을 활용해 단일 문서 업데이트로 전환할 수 있습니다. 마지막으로 db.currentOp(), Profiler, MongoDB Ops Manager 같은 도구를 통해 락 경합 지점을 모니터링하고, 쿼리 및 스키마를 꾸준히 튜닝하는 과정이 필요합니다.
만약 샤딩(Sharding) 환경에서 멀티 도큐먼트 트랜잭션을 사용한다면, 여러 샤드에 분산된 데이터를 동시에 업데이트해야 하므로 2PC(2-Phase Commit) 방식으로 커밋이 진행됩니다. 이는 Replica Set 기반 트랜잭션에 비해 더 많은 네트워크 통신과 메타데이터 잠금을 발생시키므로, 성능 비용이 커질 수 있습니다. 따라서 데이터가 다양한 샤드에 걸쳐 저장되어 있어도 모든 연산을 트랜잭션 한 번에 처리하기보다는, 샤딩 키 선정과 컬렉션 구조를 면밀히 설계해 락 범위를 최소화하는 전략이 필수적입니다.
MongoDB의 멀티 문서 트랜잭션은 RDB와 유사한 ACID 보장을 원하는 개발자들에게 반가운 기능입니다. 그러나 무조건 트랜잭션을 쓰면 간편해진다고 생각하기보다는, 그 뒤에 따라오는 성능 상의 부담과 동시성 제어의 복잡도를 인지해야 합니다.
짧고 필요한 범위 내에서만 트랜잭션을 적용하고, 기존 RDB 사고방식에서 벗어나 MongoDB의 스키마 설계나 문서 중심 특성을 적극 활용한다면, 트랜잭션을 사용하더라도 높은 성능과 확장성을 유지할 수 있습니다. 다음 파트에서는 샤딩(Sharding) 환경에서의 동시성 제어를 다루며, 대규모 트래픽과 데이터 분산 상황에서 트랜잭션이 어떻게 처리되는지 더 자세히 알아보겠습니다.