MongoDB 트랜잭션의 진화: WiredTiger, Snapshot Isolation, 스키마 설계 — Part 3
"MongoDB는 트랜잭션이 없다"는 2013년의 이야기다. 2026년 현재 MongoDB는 WiredTiger 기반 MVCC, 멀티 도큐먼트 ACID 트랜잭션, 샤딩 클러스터 분산 트랜잭션까지 지원한다. session·readConcern·writeConcern을 명시적으로 다뤄야 하는 사용 방식, PostgreSQL MVCC와의 차이, 그리고 트랜잭션 필요성 자체를 줄이는 임베딩 설계 철학을 한 편에 정리한다.
시리즈 구성 — 트랜잭션? PostgreSQL과 MongoDB의 차이점은?!
- Part 1 — 트랜잭션의 기본 개념과 ACID 원칙
- Part 2 — PostgreSQL 트랜잭션 심화 (MVCC, 격리 수준, 데드락, VACUUM)
- Part 3 — MongoDB의 트랜잭션 진화와 멀티 도큐먼트 트랜잭션 (현재 편)
- Part 4 — 실전 비교 — 성능, 확장성, 어떤 DB를 선택해야 할까?
목차
- 들어가며 — "MongoDB는 트랜잭션이 없다"는 옛말
- MongoDB 트랜잭션의 역사: 버전별 진화
- WiredTiger: MongoDB ACID의 심장
- 3-1. WiredTiger와 MVCC
- 3-2. Snapshot Isolation — PostgreSQL과 무엇이 다른가?
- 멀티 도큐먼트 트랜잭션 실전
- 4-1. 트랜잭션 기본 구조
- 4-2. Read Concern / Write Concern
- 4-3. 트랜잭션의 제약과 한계
- MongoDB의 진짜 강점: 트랜잭션보다 스키마 설계
- 5-1. 단일 도큐먼트의 원자성
- 5-2. Embedding vs Referencing
- Oplog: MongoDB 내구성의 핵심
- PostgreSQL MVCC vs MongoDB MVCC 비교
- Part 3 정리
- 실무 적용 노트
1. 들어가며 — "MongoDB는 트랜잭션이 없다"는 옛말
2013년 무렵, MongoDB를 처음 배우는 개발자라면 반드시 이 경고를 들었다.
"MongoDB는 트랜잭션을 지원하지 않습니다. 여러 도큐먼트에 걸친 일관성이 필요하다면 애플리케이션 레벨에서 직접 처리해야 합니다."
그 시절의 이야기다. 2026년 현재, MongoDB는 이미 멀티 도큐먼트 ACID 트랜잭션, 샤딩 클러스터 분산 트랜잭션, Snapshot Isolation 등을 완벽히 지원한다. 단순한 NoSQL 문서 저장소에서 엔터프라이즈급 일관성을 갖춘 데이터베이스로 진화한 것이다.
하지만 여기서 중요한 질문이 하나 생긴다.
"트랜잭션을 지원한다고 해서 PostgreSQL처럼 쓰면 될까?"
그 답은 "아니다"에 가깝다. MongoDB의 트랜잭션은 PostgreSQL과 구현 방식도, 철학도, 권장 사용 패턴도 다르다. 이번 파트에서는 MongoDB가 어떻게 트랜잭션을 구현했는지, 그리고 어떤 상황에서 어떻게 써야 하는지 낱낱이 파헤친다.
2. MongoDB 트랜잭션의 역사: 버전별 진화
MongoDB의 트랜잭션 지원은 하루아침에 이루어진 것이 아니다. 수년에 걸친 스토리지 엔진 교체와 점진적 기능 확장의 결과다.
| 버전 | 출시 연도 | 트랜잭션 관련 주요 변화 |
|---|---|---|
| ~3.x | ~2017 | 단일 도큐먼트 원자성만 지원. 멀티 도큐먼트 트랜잭션 없음 |
| 4.0 | 2018 | Replica Set 환경에서 멀티 도큐먼트 ACID 트랜잭션 최초 도입 |
| 4.2 | 2019 | **Sharded Cluster(분산 환경)**까지 트랜잭션 지원 확장 |
| 4.4 | 2020 | 트랜잭션 내 컬렉션 및 인덱스 생성 허용 |
| 5.0+ | 2021~ | 시계열 컬렉션, 클러스터 레벨 스냅샷 읽기 강화 |
| 6.0+ | 2022~ | Queryable Encryption, Change Streams 개선 |
| 7.0+ | 2023~ | Atlas Vector Search 통합, 대용량 트랜잭션 처리 개선 |
| 8.0 / Atlas 2025 | 2024-2025 | 벌크 쓰기 성능 개선, AI 네이티브 쿼리 통합 |
핵심 전환점: WiredTiger 도입
MongoDB 트랜잭션의 역사에서 가장 중요한 사건은 **WiredTiger 스토리지 엔진 도입(v3.2, 기본값 설정)**이다. 구형 MMAP 엔진은 컬렉션 단위 락을 사용했지만, WiredTiger는 도큐먼트 수준 동시성 제어와 MVCC를 지원함으로써 진정한 ACID 기반의 멀티 도큐먼트 트랜잭션 구현이 가능해졌다.
3. WiredTiger: MongoDB ACID의 심장
3-1. WiredTiger와 MVCC
WiredTiger는 MongoDB가 트랜잭션 지원을 위해 선택한 스토리지 엔진으로, **MVCC(Multi-Version Concurrency Control)**를 내장하고 있다.
WiredTiger가 데이터 변경을 처리하는 방식은 PostgreSQL의 MVCC와 개념적으로 유사하다. 기존 데이터를 덮어쓰지 않고 새 버전을 생성하며, 각 트랜잭션은 자신의 스냅샷 타임스탬프를 기준으로 데이터를 읽는다.
트랜잭션 A가 시작하면:
→ WiredTiger 스냅샷 타임스탬프 T1 할당
→ 트랜잭션 전체에서 T1 시점의 데이터 스냅샷을 일관되게 읽음
→ 다른 트랜잭션이 커밋해도 A의 스냅샷은 변하지 않음
그러나 PostgreSQL MVCC와 결정적 차이가 있다. WiredTiger는 구버전 데이터를 테이블 내부가 아닌 별도의 History Store에 보관한다. PostgreSQL이 Dead Tuple을 테이블 파일 안에 남기고 VACUUM으로 청소하는 반면, WiredTiger는 History Store를 자동으로 관리한다.
PostgreSQL은 VACUUM이 별도로 필요하지만, MongoDB(WiredTiger)는 별도 청소 프로세스가 필요 없다. 이것이 운영 측면에서 중요한 차이점이다.
3-2. Snapshot Isolation — PostgreSQL과 무엇이 다른가?
MongoDB의 멀티 도큐먼트 트랜잭션은 **Snapshot Isolation(스냅샷 격리)**을 제공한다. SQL 표준 용어로 매핑하면 REPEATABLE READ에 가장 가깝다.
MongoDB 트랜잭션의 격리 수준 (Read Concern: snapshot 기준):
Dirty Read 방지
Non-Repeatable Read 방지
Phantom Read 방지 (스냅샷 기반이므로)
Write Skew는 발생 가능 (Serializable이 아님)
PostgreSQL은 SERIALIZABLE 격리 수준(SSI 알고리즘)까지 지원하지만, MongoDB 트랜잭션의 최고 격리 수준은 Snapshot Isolation이다. 따라서 Write Skew 이상 현상이 중요한 금융 도메인에서는 이 점을 인지해야 한다.
Write Skew 예시: 두 트랜잭션이 동시에 동일한 데이터를 읽고, 서로 다른 도큐먼트를 수정할 때 논리적 제약이 깨지는 현상. 예를 들어 "당직 의사는 최소 1명 이상이어야 한다"는 제약이 있을 때, 두 의사가 동시에 "다른 의사가 있으니 나는 빠져도 된다"고 판단하여 모두 당직을 취소하는 상황.
4. 멀티 도큐먼트 트랜잭션 실전
4-1. 트랜잭션 기본 구조
MongoDB의 트랜잭션은 반드시 Session을 통해 시작된다. 이것이 PostgreSQL과 가장 큰 사용 방식의 차이다. PostgreSQL은 BEGIN 한 줄로 트랜잭션이 시작되지만, MongoDB는 클라이언트 세션을 명시적으로 생성하고 관리해야 한다.
Node.js (MongoDB Driver) 예시
const { MongoClient } = require('mongodb');
const client = new MongoClient(uri);
async function transferFunds(fromId, toId, amount) {
// 1. 세션 시작 (필수!)
const session = client.startSession();
try {
// 2. 트랜잭션 시작
session.startTransaction({
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority' }
});
const accounts = client.db('bank').collection('accounts');
// 3. 출금
await accounts.updateOne(
{ _id: fromId, balance: { $gte: amount } },
{ $inc: { balance: -amount } },
{ session } // 반드시 session 전달!
);
// 4. 입금
await accounts.updateOne(
{ _id: toId },
{ $inc: { balance: amount } },
{ session }
);
// 5. 커밋
await session.commitTransaction();
} catch (error) {
// 6. 실패 시 롤백
await session.abortTransaction();
throw error;
} finally {
// 7. 세션 종료 (메모리 누수 방지!)
await session.endSession();
}
}
Python(PyMongo) 예시 — with 문으로 더 안전하게
from pymongo import MongoClient
client = MongoClient(uri)
def transfer_funds(from_id, to_id, amount):
with client.start_session() as session:
with session.start_transaction():
accounts = client['bank']['accounts']
accounts.update_one(
{'_id': from_id, 'balance': {'$gte': amount}},
{'$inc': {'balance': -amount}},
session=session
)
accounts.update_one(
{'_id': to_id},
{'$inc': {'balance': amount}},
session=session
)
# with 블록 정상 종료 시 자동 commitTransaction
# 예외 발생 시 자동 abortTransaction
4-2. Read Concern / Write Concern
MongoDB는 PostgreSQL의 격리 수준과 다르게 Read Concern과 Write Concern이라는 두 축으로 일관성을 제어한다. 이는 분산 환경(Replica Set)을 전제로 설계된 MongoDB의 특성에서 비롯된다.
Read Concern — "어느 시점의 데이터를 읽을 것인가?"
| Read Concern | 설명 | 트랜잭션 권장 |
|---|---|---|
local | 로컬 노드의 최신 데이터 (기본값, 롤백 가능성 있음) | 비권장 |
majority | 과반수 노드에 커밋된 데이터만 읽음 | 조건부 |
snapshot | 트랜잭션 시작 시점의 일관된 스냅샷 | 권장 |
Write Concern — "얼마나 많은 노드에 쓰여야 성공으로 볼 것인가?"
| Write Concern | 설명 | 특징 |
|---|---|---|
w: 1 | Primary 한 노드에만 쓰기 성공 | 빠르지만 롤백 위험 |
w: majority | 과반수 노드에 복제 후 성공 (권장) | 내구성 강함, 레이턴시 증가 |
w: 0 | 응답 대기 없음 (Fire and Forget) | 최고 성능, 데이터 손실 위험 |
// 트랜잭션에서 가장 안전한 설정
session.startTransaction({
readConcern: { level: 'snapshot' }, // 일관된 스냅샷 읽기
writeConcern: { w: 'majority' } // 과반수 노드에 내구성 보장
});
PostgreSQL과의 핵심 차이: PostgreSQL에서
COMMIT은 곧 WAL에 기록되어 단일 노드 기준으로 즉시 내구성이 보장된다. MongoDB에서는w: majority를 써야만 복제 클러스터 전체 관점에서 동일한 수준의 내구성이 보장된다.
4-3. 트랜잭션의 제약과 한계
MongoDB 트랜잭션은 강력하지만, 알아두어야 할 중요한 제약 사항들이 있다.
실행 환경 제약
- Standalone 서버에서는 트랜잭션 불가
- Replica Set 필수 (개발 환경도 단일 노드 Replica Set으로 구성 권장)
- Sharded Cluster는 MongoDB 4.2 이상 필요
시간 제한
// 기본 트랜잭션 타임아웃: 60초
// 초과 시 자동 중단 (aborted by cleanup process)
// 조정 가능하지만 늘릴수록 WiredTiger 캐시 압박 증가
db.adminCommand({
setParameter: 1,
transactionLifetimeLimitSeconds: 30 // 기본 60초
})
WiredTiger 캐시 압박
트랜잭션이 진행되는 동안 WiredTiger는 트랜잭션 시작 시점의 스냅샷을 메모리에 유지해야 한다. 트랜잭션이 길어질수록, 또는 동시에 열린 트랜잭션이 많아질수록 캐시에 미커밋 쓰기가 누적되어 전체 클러스터 성능이 저하될 수 있다.
MongoDB 공식 권장 사항:
- 트랜잭션 내에서 수정하는 도큐먼트 수를 최소화
- 가능하면 트랜잭션을 1초 이내에 완료
- 트랜잭션 내 대용량 배치 처리는 금물
기능 제약
// 트랜잭션 내에서 사용 불가한 주요 기능들:
// - $graphLookup (샤딩된 컬렉션 대상)
// - createCollection (샤딩 클러스터 cross-shard 쓰기 시)
// - 샤딩 키 변경
// - 일부 인덱스 빌드 작업
5. MongoDB의 진짜 강점: 트랜잭션보다 스키마 설계
여기서 가장 중요한 관점 전환이 필요하다.
"MongoDB에서 트랜잭션이 필요한 상황이 생겼다면, 먼저 스키마 설계를 다시 검토하라."
이는 MongoDB 공식 문서와 설계 철학의 핵심 메시지다. MongoDB는 잘 설계된 단일 도큐먼트 구조로 대부분의 트랜잭션 필요성을 원천적으로 제거할 수 있도록 설계되었다.
5-1. 단일 도큐먼트의 원자성
MongoDB에서 단일 도큐먼트에 대한 모든 작업은 항상 원자적이다. 버전 4.0 이전에도, 이후에도 변하지 않는 기본 원칙이다.
// 이 하나의 업데이트는 항상 원자적으로 실행됨 (트랜잭션 불필요)
await orders.updateOne(
{ _id: orderId },
{
$set: { status: 'shipped' },
$push: { history: { action: 'shipped', timestamp: new Date() } },
$inc: { shipmentCount: 1 }
}
);
// $set, $push, and $inc execute as one atomic operation
5-2. Embedding vs Referencing — 트랜잭션을 줄이는 설계
MongoDB 스키마 설계의 핵심은 "함께 읽히는 데이터는 함께 저장한다" 는 원칙이다.
나쁜 설계 (RDB 방식 그대로 적용)
Collection: orders Collection: order_items Collection: payments
{ _id: "ORD001" } -> { orderId: "ORD001" } + { orderId: "ORD001" }
{ orderId: "ORD001" }
이 구조에서 주문 + 상품 + 결제를 동시에 업데이트하려면 멀티 도큐먼트 트랜잭션이 필수다.
좋은 설계 (MongoDB 임베딩 활용)
// orders 컬렉션 하나에 모든 관련 데이터를 내포
{
_id: "ORD001",
userId: "USR123",
status: "confirmed",
items: [
{ productId: "P001", name: "노트북", qty: 1, price: 1200000 },
{ productId: "P002", name: "마우스", qty: 2, price: 35000 }
],
payment: {
method: "card",
amount: 1270000,
paidAt: ISODate("2026-04-22T09:00:00Z")
},
history: [
{ action: "created", at: ISODate("2026-04-22T08:55:00Z") },
{ action: "confirmed", at: ISODate("2026-04-22T09:00:00Z") }
]
}
이 구조에서 주문 상태 변경, 결제 정보 업데이트, 이력 추가 모두 단일 updateOne으로 원자적 처리 가능 — 트랜잭션 불필요.
임베딩 vs 참조 선택 기준
| 상황 | 권장 방식 |
|---|---|
| 항상 함께 조회되는 데이터 | Embedding (내포) |
| 독립적으로 조회되는 데이터 | Referencing (참조) |
| 1:N 관계, N이 소수 (주문-상품) | Embedding |
| 1:N 관계, N이 무한정 증가 (게시글-댓글) | Referencing |
| 16MB 도큐먼트 제한 초과 우려 | Referencing |
| 데이터 중복이 허용되고 읽기 성능 중요 | Embedding + 비정규화 |
6. Oplog: MongoDB 내구성의 핵심
PostgreSQL이 **WAL(Write-Ahead Log)**로 내구성을 보장한다면, MongoDB는 **Oplog(Operations Log)**가 그 역할을 담당한다.
트랜잭션 관점에서 Oplog의 역할:
- 트랜잭션 내 모든 쓰기 작업은 커밋 시 단일 또는 다수의 Oplog 엔트리로 기록된다
- MongoDB 4.2 이전: 트랜잭션 전체가 하나의 16MB BSON 도큐먼트 제한에 묶였음
- MongoDB 4.2 이후: 필요한 만큼 다수의 Oplog 엔트리 생성으로 사실상 크기 제한 해소
// 트랜잭션 커밋 시 Oplog 기록 구조 (개념)
{
op: "tx",
ts: Timestamp(1682000000, 1),
lsid: { id: UUID("...") }, // 세션 ID
txnNumber: NumberLong(1), // 트랜잭션 번호
applyOps: [ // 트랜잭션 내 모든 작업
{ op: "u", ns: "bank.accounts", ... },
{ op: "u", ns: "bank.accounts", ... }
]
}
7. PostgreSQL MVCC vs MongoDB MVCC 비교
두 데이터베이스 모두 MVCC를 사용하지만, 구현 방식과 운영 특성에서 차이가 있다.
| 비교 항목 | PostgreSQL | MongoDB (WiredTiger) |
|---|---|---|
| 구버전 데이터 위치 | 테이블 파일 내 Dead Tuple | 별도 History Store |
| 구버전 정리 방식 | VACUUM (수동/AutoVacuum) | 자동 정리 (별도 프로세스 불필요) |
| 최대 격리 수준 | Serializable (SSI) | Snapshot Isolation |
| Write Skew 방지 | 가능 (SERIALIZABLE) | 불가 |
| 트랜잭션 시작 방법 | BEGIN; | 세션 생성 + startTransaction() |
| 격리 제어 방식 | Isolation Level (SQL 표준) | Read Concern + Write Concern |
| 분산 트랜잭션 | 확장 필요 (Citus 등) | 4.2부터 네이티브 지원 |
| 쓰기 충돌 처리 | 락 대기 (Blocking) | 즉시 중단 후 재시도 (Non-blocking) |
| 운영 복잡도 | AutoVacuum 튜닝 필수 | 상대적으로 단순 |
특히 쓰기 충돌 처리 방식의 차이는 실무에서 중요하다. PostgreSQL은 충돌 시 락을 잡고 대기하는 반면, MongoDB는 충돌을 즉시 감지하고 재시도 가능한 예외를 던진다.
// MongoDB 쓰기 충돌 재시도 패턴
async function runWithRetry(txnFunc, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const session = client.startSession();
try {
session.startTransaction();
await txnFunc(session);
await session.commitTransaction();
return; // 성공
} catch (err) {
await session.abortTransaction();
// TransientTransactionError는 재시도 가능
if (err.errorLabels?.includes('TransientTransactionError') && attempt < maxRetries - 1) {
continue;
}
throw err;
} finally {
session.endSession();
}
}
}
8. Part 3 정리
MongoDB의 트랜잭션 여정을 정리하면 다음과 같다.
핵심 요약:
- 역사: 단일 도큐먼트 원자성 → 4.0 Replica Set 멀티 트랜잭션 → 4.2 분산 트랜잭션 → 현재 엔터프라이즈급 완성
- WiredTiger: History Store 기반 MVCC. PostgreSQL처럼 VACUUM이 필요 없음
- 격리 수준: Snapshot Isolation (REPEATABLE READ 수준). Serializable은 미지원
- Read/Write Concern: 분산 환경을 위한 MongoDB 고유의 일관성 제어 축
- 트랜잭션 제약: 60초 시간 제한, Replica Set 필수, WiredTiger 캐시 압박 주의
- 스키마 설계가 우선: 잘 설계된 임베딩 구조로 트랜잭션 필요성 자체를 줄이는 것이 MongoDB 철학
Part 4에서는 PostgreSQL과 MongoDB를 성능, 확장성, 실제 사용 사례 기준으로 직접 비교하고, 어떤 상황에서 무엇을 선택해야 하는가에 대한 명확한 가이드를 제시한다.
9. 실무 적용 노트
참고 자료
- MongoDB 공식 문서: Transactions
- MongoDB 공식 문서: Production Considerations
- MongoDB Blog: Multi-Document ACID Transactions GA
- DZone: Isolation Level for MongoDB Multi-Document Transactions
- DEV.to: MongoDB Read/Write vs PostgreSQL Synchronous Replication
- Medium: Understanding MongoDB Transactions Atomicity
- MongoDB: 6 Rules of Thumb for Schema Design
- Mafiree: MongoDB Transactions Comprehensive Guide (2026)