Read-Modify-Write 격리 in NoSQL, part 2: 불변 조건이 여러 aggregates에 걸칠 때
Source: Dev.to
번역하려는 텍스트를 제공해 주시면, 요청하신 대로 한국어로 번역해 드리겠습니다. (코드 블록, URL 및 마크다운 형식은 그대로 유지됩니다.)
소개
파트 1에서는 단일 문서 경우를 살펴보았으며, 여기서 낙관적 잠금은 간단한 버전 필드로 여러분을 보호합니다. 이제 그 편안함을 깨는 경계선을 넘어갑니다.
The Scenario
Your product sells seats, and an organization buys a license capped at 100 seats. Those seats are spread across many Teams, and each Team is its own aggregate with its own lifecycle. You can’t stuff the list of all teams into one document: it grows unbounded and violates every aggregate‑design instinct you have.
Invariant:
Σ(team.seats) ≤ 100
The sum of seats over all Team aggregates must never exceed 100.
순진한 접근법과 그 함정
모든 “팀에 좌석 추가” 작업에서 정직한 방법은 현재 상태를 모든 팀에 대해 읽고 합산하는 것입니다—즉, N개의 팀 문서를 스캔하는 팬아웃이며 조직이 커질수록 느려집니다.
트랜잭션 안에서 실제 현재 팀들을 읽는 것이 옳다고 느껴지지만, 그 직관이 함정입니다.
쓰기‑왜곡 예시
guard: Σ seats over all Team docs = 90 (max 100)
Tx A reads all teams → Σ = 90 → 90+8 ≤ 100 ✅ → writes Team Alpha (+8) → COMMIT
Tx B reads all teams → Σ = 90 → 90+8 ≤ 100 ✅ → writes Team Beta (+8) → COMMIT
└─ each reads its OWN snapshot — neither sees the other's in‑flight write.
Two DIFFERENT Team docs → no write‑write conflict to abort.
결과:
Σ seats across Team aggregates = 106 > license cap 100
문서 간에 충돌이 없었고, 덮어쓰기도 일어나지 않았습니다. 불변식이 문서들 사이에서 깨졌습니다. 이것이 쓰기 왜곡(write skew) 입니다.
각 트랜잭션은 유효한 상태를 읽고 지역적으로 올바른 결정을 내렸지만—실제 총합은 이제 106이 되었고, 100좌석 라이선스를 초과 판매한 것입니다.
Lost Update vs. Write Skew
- Lost update – 두 개의 쓰기가 같은 문서에 도달해 하나가 다른 것을 지우고, 저장된 값 자체가 잘못됩니다.
- Write skew – 두 개의 쓰기가 서로 다른 문서에 도달해 각각 정상적으로 커밋되고, 각 문서는 내부적으로 일관성을 유지하지만, 문서 간 불변식이 깨집니다.
Source: …
MongoDB에서 트랜잭션
전체를 네이티브 MongoDB 다문서 트랜잭션(레플리카 세트에서는 ≥ 4.0, 샤드 클러스터에서는 4.2+)으로 감싸면 스냅샷 격리가 제공됩니다: 모든 트랜잭션은 데이터베이스의 일관된 시점 스냅샷을 보게 됩니다. 이는 고전적인 ANSI 읽기 이상(더티 리드, 비반복 읽기, 팬텀)과 단일 문서 손실 업데이트를 제거합니다.
하지만 스냅샷 격리는 완전한 직렬화 가능성을 제공하지 않으므로 쓰기 스큐를 방지할 수 없습니다. 두 트랜잭션이 각각 일관된 스냅샷을 보고 유효한 결정을 내리더라도 불변식을 위반할 수 있습니다.
스토리지 엔진(WiredTiger)은 두 트랜잭션이 같은 문서를 동시에 쓸 때만 트랜잭션을 중단합니다. 예시에서 Tx A는 Team Alpha를 쓰고, Tx B는 Team Beta를 쓰는데—문서가 다르기 때문에 WiredTiger가 중단할 이유가 없습니다. 제약 조건은 엔진이 충돌을 감시하는 데이터가 아니라 여러분의 코드에 존재합니다.
엔진에 불변식 표시하기
수정 사항은 불변식을 엔진이 충돌을 감시하는 대상의 일부로 만들어야 합니다. 일반적인 패턴은 불변식을 단일 문서로 구체화하여 모든 작성자가 해당 문서를 함께 수정하도록 하는 것입니다:
{
"license": {
"usedSeats": 90,
"maxSeats": 100
}
}
이제 보이지 않던 왜곡이 엔진이 감지할 수 있는 쓰기‑쓰기 충돌이 됩니다.
요약
Write skew은 트랜잭션의 결함이 아니며, MongoDB가 “잘못된” 것은 아닙니다. Write skew은 불변식이 데이터베이스의 충돌‑감지 모델에 포함되지 않을 때 발생합니다—read scope(모든 Team 문서)와 write scope(업데이트하는 단일 문서) 사이의 불일치입니다. 모든 해결책은 이러한 쓰기를 직렬화해야 하며, 비용이 들지 않는 것은 없습니다.
다음 단계
많은 개발자들이 처음 시도하는 반응은 distributed lock(예: Redis 사용)으로 읽기와 쓰기 주변의 세계를 고정하는 것입니다. 이것은 이론적으로 직렬화를 달성할 수 있지만, 지연 시간, 교착 상태 및 TTL 딜레마를 초래합니다.
논의는 Part 3에서 계속되며, 여기서 우리는 보다 견고한 솔루션을 탐구합니다.