MongoDB의 ACID 완전 정복 Part 3 — Isolation: Snapshot Isolation과 동시성
MongoDB에서 격리(Isolation)는 MVCC와 Read Concern으로 구현됩니다. 이 글에서는 더티 리드·반복 불가 읽기·팬텀·리드 스큐를 짚고, SQL 격리 레벨에 무리하게 끼워 맞추지 않고 공식 문서의 말을 어떻게 읽을지 정리합니다. 멀티 도큐먼트 트랜잭션에서 스냅샷이 보장하는 범위와, fail-on-conflict·재시도·잠금 대기 타임아웃이 한 그림에서 어떻게 연결되는지 설명합니다. Read Concern `local`·`majority`·`snapshot`의 차이, 복제 환경에서의 주의점, Causal Consistency가 성립하기 위한 전제, Write Skew와 완화 패턴의 트레이드오프까지 실무 질문에 연결합니다. 이후 Part 4에서는 Durability와 Write Concern으로 이어집니다.
시리즈 구성
- Part 1 | ACID 개념 + MongoDB의 역사적 맥락
- Part 2 | Atomicity·Consistency — 단일 문서부터 멀티 문서
- Part 3 ← 지금 여기 | Isolation과 Snapshot Isolation 내부 동작
- Part 4 | Durability, WiredTiger, Write Concern (출간 예정)
- Part 5 | 실전 코드 패턴 + 성능 최적화 + 안티패턴 (출간 예정)
목차
- 들어가며
- 격리 이상 현상(Isolation Anomaly) 네 가지
- SQL 표준 격리 레벨과 MongoDB — 왜 표에만 믿기 어려운가
- MVCC — MongoDB가 동시성을 다루는 방식
- Snapshot Isolation 내부 동작 — 보장의 경계
- Read Concern — 읽기 일관성 조절
- Write Conflict: fail-on-conflict와 잠금 대기 타임아웃
- Causal Consistency — 전제를 함께 두고 읽기
- 실전 예 — Read Concern과 트랜잭션
- 한계와 주의 — Write Skew와 완화 패턴
- 마치며
1. 들어가며
ACID 네 속성 가운데 Isolation(격리성) 은 개념이 많고, 오해도 많으며, 동시성 버그와도 잘 엮입니다.
Part 2에서는 Atomicity·Consistency와 함께 readConcern과 writeConcern을 한 세트로 읽어야 한다고 짚었습니다. 여기서 한 걸음 좁혀, 이번 글은 격리(I) 에 해당하는 질문 — “동시에 실행되는 트랜잭션 사이에 무엇이 보이고, 무엇이 가려지는가” — 에 맞춥니다. 읽기 일관성을 조절하는 Read Concern과 스냅샷·MVCC를 중심으로 설명하고, Write Concern이 커밋을 어디까지 영속화할지에 가깝다는 축은 Part 4(Durability)에서 이어갑니다.
"커밋된 데이터만 읽으면 되지 않나요?"라고 생각하기 쉽지만, 어떤 시점의 커밋된 데이터를 볼 것인지가 격리의 핵심입니다. MongoDB의 격리 모델을 한 줄로 압축하면 대략 다음과 같습니다.
MVCC 기반 Snapshot Isolation — 경합 시 무한 대기 대신 fail-on-conflict와 재시도
이 문장이 무엇을 뜻하는지, 그리고 트랜잭션 안과 일반 단일 연산에서 보장이 어떻게 달라지는지를 이번 글에서 차근차근 풀어 보겠습니다.
2. 격리 이상 현상(Isolation Anomaly) 네 가지
동시에 여러 트랜잭션이 돌아갈 때 흔히 인용되는 네 가지입니다.
2.1 Dirty Read (더티 리드)
아직 커밋되지 않은 값을 읽는 현상입니다. 상대 트랜잭션이 롤백하면, 읽은 값은 존재하지 않았던 것이 됩니다.
2.2 Non-Repeatable Read (반복 불가능한 읽기)
같은 트랜잭션 안에서 같은 조건으로 두 번 읽었을 때 결과가 달라지는 현상입니다.
2.3 Phantom Read (팬텀 리드)
같은 조건으로 두 번 읽었을 때 행(또는 도큐먼트) 집합이 달라지는 현상입니다. 삽입·삭제 때문에 결과 개수가 바뀌는 경우를 흔히 말합니다.
2.4 Read Skew (읽기 스큐)
연관된 여러 값을 읽을 때, 읽기 시점이 어긋나 논리적으로 모순된 조합을 보게 되는 현상입니다. 예를 들어 A·B 잔액을 따로 읽다가, 그 사이에 다른 트랜잭션이 이체를 커밋하면 합계가 맞지 않는 "중간 세계"가 될 수 있습니다.
이 네 가지를 기준으로 삼아야, 이후에 나오는 Snapshot Isolation과 Read Concern의 의미가 분명해집니다.
3. SQL 표준 격리 레벨과 MongoDB — 왜 표에만 믿기 어려운가
SQL 표준은 잠금 중심 구현을 염두에 둔 격리 수준(READ UNCOMMITTED ~ SERIALIZABLE)을 정의합니다. MongoDB는 WiredTiger MVCC를 쓰므로, "MongoDB = REPEATABLE READ"처럼 한 칸에 끼워 넣는 매핑은 설명을 오히려 흐릴 수 있습니다.
실무에서는 다음을 권합니다.
- SQL 격리 표를 MongoDB의 최종 답으로 쓰지 않는다.
- 대신 Read Concern·트랜잭션 여부·배포(standalone / replica set / sharded) 를 함께 읽는다.
3.1 local을 SQL의 READ UNCOMMITTED과 동일시하지 않기
문서나 블로그에서 local을 "READ UNCOMMITTED에 가깝다"고 적는 경우가 있습니다. 독자가 SQL 격리 수준 표와 1:1로 대응시키면, "더티 리드가 곧바로 허용된다"는 이미지로 굳어질 수 있습니다.
MongoDB에서 local의 핵심은 특정 멤버에서 본 최신 데이터를 빠르게 읽는다는 점과, 복제·선출(primary stepdown) 과정에서 롤백될 수 있는 데이터를 읽을 위험이 있다는 점에 가깝습니다. 즉 SQL 격리 레벨 이름으로 단정하기보다, "노드 로컬 가시성 + 내구성·복제 관점의 위험"을 함께 이해하는 편이 안전합니다.
3.2 스냅샷이 "팬텀까지 사라진다"를 말할 때의 전제
멀티 도큐먼트 트랜잭션에서 readConcern: "snapshot"(트랜잭션 기본)을 쓰면, 트랜잭션 내부의 읽기들은 같은 스냅샷 위에서 일관되게 동작합니다. 여기서 말하는 보장은 그 트랜잭션 경계 안에서의 일관된 뷰에 가깝습니다.
반면 트랜잭션 밖에서 여러 번 나누어 읽거나, 읽기마다 다른 Read Concern을 쓰면 다른 시점의 데이터를 보게 될 수 있습니다. "스냅샷이면 팬텀 이상이 전부 사라진다"처럼 문맥 없이 말하면 오해의 소지가 있으니, 항상 트랜잭션·세션·read concern을 같이 적어 주는 것이 좋습니다.
4. MVCC — MongoDB가 동시성을 다루는 방식
Snapshot Isolation을 이해하려면 MVCC(Multi-Version Concurrency Control) 가 전제입니다.
4.1 기본 아이디어
갱신 시 기존 행만 덮어쓰지 않고 새 버전을 쌓아 두고, 읽기는 자신에게 허용된 버전만 본다고 이해하면 됩니다. 읽기와 쓰기가 서로를 불필요하게 오래 막지 않도록 하기 위한 설계입니다.
4.2 WiredTiger와 스냅샷
WiredTiger는 트랜잭션 ID를 기반으로 버전 가시성을 판단합니다. 트랜잭션은 시작 맥락에 따라 어떤 커밋까지 보일지가 정해지고, 그 기준으로 문서 버전을 고릅니다. 그 결과 커밋되지 않은 다른 트랜잭션의 쓰기를 읽지 않는 동작과 연결됩니다.
5. Snapshot Isolation 내부 동작 — 보장의 경계
Snapshot Isolation의 핵심은 다음 한 문장으로 요약할 수 있습니다.
한 트랜잭션 안의 읽기들은, 정해진 스냅샷 위에서 일관되게 동작한다.
그 사이에 다른 트랜잭션이 커밋을 해도, 내 트랜잭션이 잡은 스냅샷이 바뀌지 않는 한 동일 트랜잭션 내 조회 결과는 일관됩니다. 그래서 Read Skew 를 피하는 데 유리합니다.
5.1 이상 현상과의 관계(요지)
멀티 도큐먼트 트랜잭션에서 snapshot Read Concern을 쓰는 전형적인 경우, 더티 리드·동일 트랜잭션 내 비반복 읽기·리드 스큐 에 강하게 대응하는 쪽으로 이해할 수 있습니다. 다만 Write Skew 는 별개로, 아래 10절에서 다룹니다.
6. Read Concern — 읽기 일관성 조절
MongoDB는 Read Concern으로 읽기 일관성을 조절합니다. 운영에서 자주 마주치는 레벨만 짚겠습니다.
6.1 local
- 연결이 붙은 멤버에서 가장 최근에 볼 수 있는 데이터를 읽습니다.
- 속도는 빠른 편이지만, 복제 지연이나 장애 시나리오와 맞물리면 나중에 롤백될 수 있는 데이터를 읽었을 가능성을 염두에 두어야 합니다.
- 금융·재고처럼 강한 내구성 전제가 필요하면 기본값 그대로
local에만 의존하는 설계는 재검토가 필요합니다.
6.2 majority
- 복제 세트 과반에게 확인된 데이터만 읽습니다.
- "롤백되지 않을 데이터를 읽는다"는 관점에서
local보다 보수적입니다. 비트랜잭션 읽기에서도 자주 고려됩니다.
6.3 snapshot (멀티 도큐먼트 트랜잭션에서의 기본)
- 트랜잭션에 대해 일관된 스냅샷을 제공합니다.
- 트랜잭션 내부의 읽기 일관성을 맞추는 중심 축입니다.
6.4 linearizable
- 매우 강한 조건의 읽기로, 지연과 제약(예: 단일 문서 읽기 권장 등)이 큽니다. 용도가 한정적입니다.
6.5 트랜잭션에서는 연산별 Read Concern이 아니라 트랜잭션 옵션
멀티 도큐먼트 트랜잭션에서는 개별 find에 read concern을 붙여 바꾼다기보다, startTransaction / withTransaction에 넘긴 옵션이 트랜잭션 전체를 지배하는 쪽으로 이해하는 것이 안전합니다. (드라이버·버전에 따라 세부 동작을 확인하세요.)
await session.withTransaction(
async () => {
const doc = await collection.findOne({ _id: 1 }, { session });
// ...
},
{
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" },
}
);
7. Write Conflict: fail-on-conflict와 잠금 대기 타임아웃
PostgreSQL 등 잠금 기반 DB는 같은 데이터에 대한 쓰기가 겹치면 한쪽이 끝날 때까지 기다리는 방식이 흔합니다. MongoDB(WiredTiger)의 문서 수준 갱신에서는 즉시 충돌로 실패하고, 애플리케이션이 재시도하는 모델에 가깝습니다. 이것이 흔히 말하는 fail-on-conflict 입니다.
7.1 왜 기다림을 무한히 두지 않는가
순환 대기를 허용하면 데드락 해소 비용이 커질 수 있습니다. MongoDB는 대기로 데드락을 푸는 대신, 충돌을 빨리 드러내고 클라이언트 재시도에 맡기는 쪽을 택했습니다.
7.2 withTransaction과 재시도
session.withTransaction()은 TransientTransactionError 등 일시적 오류에 대해 재시도를 시도하는 경로가 있습니다. 운영 코드에서는 재시도 상한·백오프·멱등성까지 설계에 포함하는 것이 좋습니다.
7.3 fail-on-conflict와 maxTransactionLockRequestTimeoutMillis 는 같은 이야기가 아니다
여기서 흔한 혼동이 있습니다. 문서 간 쓰기 충돌로 트랜잭션이 바로 실패하는 것과, 잠금을 잡기 위해 아주 짧게 대기했다가 포기하는 것은 다른 층입니다.
- Write conflict: 이미 다른 트랜잭션이 수정 중인 데이터와 충돌하면 재시도가 필요한 실패로 돌아오는 경우가 많습니다.
- 잠금 대기 타임아웃(
maxTransactionLockRequestTimeoutMillis, 기본값은 환경·버전 문서 확인): 잠금을 얻기 위해 짧게 기다리다가 못 얻으면 포기할 수 있습니다.
즉 "기다리지 않는다"와 "5ms까지는 잠금을 기다릴 수 있다"는 서로 다른 단계입니다. 한 단락으로 섞어 말하면 모순처럼 보일 수 있으니, 운영 문서를 읽을 때도 충돌 처리와 잠금 획득을 나누어 생각하면 이해가 쉽습니다.
8. Causal Consistency — 전제를 함께 두고 읽기
Causal Consistency는 "내가 쓴 뒤에는, 같은 세션에서 읽을 때 그 인과가 보인다"는 류의 기대를 다룹니다. 다만 이것이 아무 설정 없이 모든 Secondary에서 자동으로 보장된다는 뜻은 아닙니다.
다음은 이해를 돕기 위한 체크리스트입니다.
- 클라이언트 세션을 일관되게 사용하는가?
- read / write concern이 인과 전파에 필요한 수준으로 맞춰져 있는가? (예: 쓰기
majority등) - read preference가 Secondary로 향할 때, 복제 지연과의 관계를 수용하는가?
세션을 끊거나, 읽기만 local에 두고 쓰기만 majority에 두는 등 설정이 파편화되면, 기대한 "바로 직전 쓰기가 보인다"가 깨질 수 있습니다. 운영 환경에서는 드라이버 문서와 매뉴얼의 causal consistency 절을 버전에 맞게 확인하는 것이 좋습니다.
const session = client.startSession({ causalConsistency: true });
await collection.updateOne(
{ _id: userId },
{ $set: { city: "서울" } },
{ session, writeConcern: { w: "majority" } }
);
const user = await collection.findOne(
{ _id: userId },
{ session, readConcern: { level: "majority" } }
);
9. 실전 예 — Read Concern과 트랜잭션
이체가 진행 중일 때 잔액 합계를 읽는 상황을 가정해 보겠습니다. 중간 상태(한쪽만 차감된 상태)를 합계에 반영하고 싶지 않다면, 단순 집계 한 번에 local만 걸어서는 부족할 수 있습니다.
// T2: 합계를 '한 트랜잭션 스냅샷' 위에서 읽고 싶은 경우
const session = client.startSession();
let total;
await session.withTransaction(
async () => {
const agg = await accounts
.aggregate(
[{ $group: { _id: null, total: { $sum: "$balance" } } }],
{ session }
)
.toArray();
total = agg[0]?.total;
},
{ readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } }
);
같은 시점의 스냅샷 위에서 합계를 보려는 것이므로, 집계를 트랜잭션으로 감싸고 Read Concern을 트랜잭션 옵션으로 주는 패턴이 맞습니다. 반대로 local로 여러 번 나누어 읽으면, 서로 다른 시점의 값이 섞일 수 있습니다.
10. 한계와 주의 — Write Skew와 완화 패턴
10.1 Write Skew
Write Skew는 두 트랜잭션이 각자의 스냅샷을 보고 서로 다른 문서를 갱신한 결과, 전역 불변식이 깨지는 현상입니다. "당직 의사가 최소 한 명" 같은 규칙이 대표적인 예입니다.
Snapshot Isolation만으로는 이 유형이 자동으로 막히지 않을 수 있어, 비즈니스 규칙을 명시적으로 쓰기 충돌이 나도록 모델링하거나, 제약·조건부 갱신으로 방어선을 두는 경우가 많습니다.
10.2 더미 갱신(no-op update) 패턴과 트레이드오프
흔한 패턴으로, 읽은 문서에 아주 작은 필드를 갱신해 같은 문서에 대한 쓰기 충돌을 유도하는 방법이 있습니다. 다만 _lastChecked 같은 필드를 반복 갱신하면 감사 로그 노이즈, 쓰기 증폭, 인덱스·히트율에 영향을 줄 수 있습니다.
대안으로는 다음을 검토할 수 있습니다.
- 조건부 단일
updateMany/ 원자적 카운터로 불변식을 한 연산에 담는다. - 유니크 제약·부분 인덱스로 상태 공간을 좁힌다.
- 도메인 모델을 바꿔 경쟁 지점을 줄인다.
"패턴 한 가지"에만 의존하기보다, 운영 비용까지 포함해 고르는 편이 설득력 있습니다.
10.3 트랜잭션 내 Read Concern 혼합
한 트랜잭션 안에서 연산마다 다른 Read Concern을 섞어 쓰는 상상은 대체로 피하는 것이 좋습니다. 트랜잭션 수준 옵션을 기준으로 설계하세요.
11. 마치며
| 키 포인트 | 요약 |
|---|---|
| MVCC | 버전을 쌓아 읽기·쓰기 경합을 다룹니다. |
| Snapshot Isolation | 트랜잭션 내 읽기 일관성의 중심 축입니다. |
| fail-on-conflict | 무한 대기 대신 충돌을 드러내고 재시도로 넘깁니다. |
| Read Concern | local / majority / snapshot 등 읽기의 의미를 바꿉니다. |
| Write Skew | 스냅샷만으로는 부족할 수 있어 모델링·제약이 필요합니다. |
| Causal Consistency | 세션·concern·read preference를 함께 맞춰야 기대와 일치합니다. |
다음 Part 4에서는 Durability, WiredTiger의 저널·체크포인트, Write Concern과 장애 시나리오를 이어가겠습니다.
참고·출처
- MongoDB Manual — Transactions (진행 중 트랜잭션과 쓰기 충돌: 동일 문서에 대한 동시 갱신)
- MongoDB Manual — Read Concern
- MongoDB Manual — Read Concern "snapshot"
- MongoDB Manual — maxTransactionLockRequestTimeoutMillis
- MongoDB Manual — Causal Consistency
- MongoDB Manual — Transactions in Applications (재시도·
TransientTransactionError, Write Skew 완화) - MongoDB Manual — Write Operations Atomicity
작성: 2026년 4월 · 제품 버전·기본값·드라이버 동작은 시점에 따라 달라질 수 있으니, 인용 시 사용 중인 버전의 공식 매뉴얼을 확인하세요.