2026년 6월 4일 목요일
글 목록
Lv.2 초급MongoDB
16분 읽기Lv.2 초급
시리즈트랜잭션? PostgreSQL과 MongoDB의 차이점은?! · 파트 3시리즈 허브 보기

MongoDB 트랜잭션의 진화: WiredTiger, Snapshot Isolation, 스키마 설계 — Part 3

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를 선택해야 할까?

목차

  1. 들어가며 — "MongoDB는 트랜잭션이 없다"는 옛말
  2. MongoDB 트랜잭션의 역사: 버전별 진화
  3. WiredTiger: MongoDB ACID의 심장
    • 3-1. WiredTiger와 MVCC
    • 3-2. Snapshot Isolation — PostgreSQL과 무엇이 다른가?
  4. 멀티 도큐먼트 트랜잭션 실전
    • 4-1. 트랜잭션 기본 구조
    • 4-2. Read Concern / Write Concern
    • 4-3. 트랜잭션의 제약과 한계
  5. MongoDB의 진짜 강점: 트랜잭션보다 스키마 설계
    • 5-1. 단일 도큐먼트의 원자성
    • 5-2. Embedding vs Referencing
  6. Oplog: MongoDB 내구성의 핵심
  7. PostgreSQL MVCC vs MongoDB MVCC 비교
  8. Part 3 정리
  9. 실무 적용 노트

1. 들어가며 — "MongoDB는 트랜잭션이 없다"는 옛말

2013년 무렵, MongoDB를 처음 배우는 개발자라면 반드시 이 경고를 들었다.

"MongoDB는 트랜잭션을 지원하지 않습니다. 여러 도큐먼트에 걸친 일관성이 필요하다면 애플리케이션 레벨에서 직접 처리해야 합니다."

그 시절의 이야기다. 2026년 현재, MongoDB는 이미 멀티 도큐먼트 ACID 트랜잭션, 샤딩 클러스터 분산 트랜잭션, Snapshot Isolation 등을 완벽히 지원한다. 단순한 NoSQL 문서 저장소에서 엔터프라이즈급 일관성을 갖춘 데이터베이스로 진화한 것이다.

하지만 여기서 중요한 질문이 하나 생긴다.

"트랜잭션을 지원한다고 해서 PostgreSQL처럼 쓰면 될까?"

그 답은 "아니다"에 가깝다. MongoDB의 트랜잭션은 PostgreSQL과 구현 방식도, 철학도, 권장 사용 패턴도 다르다. 이번 파트에서는 MongoDB가 어떻게 트랜잭션을 구현했는지, 그리고 어떤 상황에서 어떻게 써야 하는지 낱낱이 파헤친다.


2. MongoDB 트랜잭션의 역사: 버전별 진화

MongoDB의 트랜잭션 지원은 하루아침에 이루어진 것이 아니다. 수년에 걸친 스토리지 엔진 교체와 점진적 기능 확장의 결과다.

버전출시 연도트랜잭션 관련 주요 변화
~3.x~2017단일 도큐먼트 원자성만 지원. 멀티 도큐먼트 트랜잭션 없음
4.02018Replica Set 환경에서 멀티 도큐먼트 ACID 트랜잭션 최초 도입
4.22019**Sharded Cluster(분산 환경)**까지 트랜잭션 지원 확장
4.42020트랜잭션 내 컬렉션 및 인덱스 생성 허용
5.0+2021~시계열 컬렉션, 클러스터 레벨 스냅샷 읽기 강화
6.0+2022~Queryable Encryption, Change Streams 개선
7.0+2023~Atlas Vector Search 통합, 대용량 트랜잭션 처리 개선
8.0 / Atlas 20252024-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 ConcernWrite Concern이라는 두 축으로 일관성을 제어한다. 이는 분산 환경(Replica Set)을 전제로 설계된 MongoDB의 특성에서 비롯된다.

Read Concern — "어느 시점의 데이터를 읽을 것인가?"

Read Concern설명트랜잭션 권장
local로컬 노드의 최신 데이터 (기본값, 롤백 가능성 있음)비권장
majority과반수 노드에 커밋된 데이터만 읽음조건부
snapshot트랜잭션 시작 시점의 일관된 스냅샷권장

Write Concern — "얼마나 많은 노드에 쓰여야 성공으로 볼 것인가?"

Write Concern설명특징
w: 1Primary 한 노드에만 쓰기 성공빠르지만 롤백 위험
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를 사용하지만, 구현 방식과 운영 특성에서 차이가 있다.

비교 항목PostgreSQLMongoDB (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)

이 글 공유하기

시리즈 내비게이션

트랜잭션? PostgreSQL과 MongoDB의 차이점은?!

현재 글 3 · 4 편 공개

같은 주제 더 보기·대표 시리즈로 시작

English

최신 글을 RSS로 받아보세요

RSS로 새 글과 시리즈 업데이트를 바로 받아볼 수 있습니다.

RSS 구독 안내 보기