---
layout: post
title: "MVCC, 동시성 제어의 마법사: 데이터를 읽고 쓰는 새로운 방법"
subtitle: "다중 버전 동시성 제어(MVCC)로 데이터베이스 성능과 일관성을 동시에 잡는 비결"
date: 2025-11-27 00:53:56.877Z +0900
background: '/img/posts/pattern01.jpg'
category: Study
tags: [database,mvcc,concurrency_control]
---
## 서론: 동시성 제어, 왜 중요할까?
데이터베이스는 여러 사용자와 애플리케이션이 동시에 접근하여 데이터를 읽고 쓰는 환경에서 작동합니다. 이러한 동시 접근 상황에서 데이터의 정확성과 일관성을 유지하는 것은 매우 중요한 과제입니다. 만약 적절한 제어 없이 동시 트랜잭션이 발생한다면, '잘못된 읽기(Dirty Read)', '반복 불가능한 읽기(Non-Repeatable Read)', '유령 읽기(Phantom Read)'와 같은 문제들이 발생하여 데이터 무결성이 손상될 수 있습니다.
전통적인 동시성 제어 방식은 주로 '잠금(Locking)'을 사용하여 데이터 접근을 제어했습니다. 쓰기 트랜잭션이 데이터를 수정하는 동안 해당 데이터에 대한 다른 트랜잭션의 접근을 막아 일관성을 보장하는 방식이죠. 하지만 이는 심각한 성능 저하, 교착 상태(Deadlock) 발생 위험, 그리고 읽기 작업이 쓰기 작업을 차단하거나 그 반대의 상황이 발생할 수 있다는 단점을 안고 있습니다.
이러한 문제들을 해결하고 동시성을 극대화하기 위해 등장한 혁신적인 기술이 바로 **다중 버전 동시성 제어(Multi-Version Concurrency Control, MVCC)**입니다.
## 본문: MVCC, 어떻게 동시성을 마법처럼 제어할까?
MVCC는 데이터베이스가 특정 데이터의 여러 버전을 동시에 유지하고, 각 트랜잭션이 자신의 시작 시점에 해당하는 데이터 스냅샷을 볼 수 있도록 함으로써 동시성 문제를 해결합니다. "읽기(Reader)는 쓰기(Writer)를 블록하지 않고, 쓰기는 읽기를 블록하지 않는다"는 철학을 기반으로 합니다.
### 1. MVCC의 핵심 원리
* **다중 버전 관리:** 데이터를 업데이트할 때 기존 데이터를 직접 덮어쓰지 않고, 새로운 버전의 데이터를 생성합니다. 이 때 각 데이터 버전은 생성된 트랜잭션 ID(`tx_start_id`)와 종료된 트랜잭션 ID(`tx_end_id`) 같은 메타데이터를 포함합니다.
* **스냅샷 격리:** 각 읽기 트랜잭션은 자신이 시작된 시점의 데이터베이스 "스냅샷"을 봅니다. 즉, 트랜잭션 시작 이후에 다른 트랜잭션이 커밋한 변경사항은 해당 읽기 트랜잭션에 보이지 않습니다.
* **잠금 없는 읽기:** 읽기 트랜잭션은 데이터에 잠금을 걸지 않고, 자신의 스냅샷에 해당하는 버전을 조회합니다. 따라서 쓰기 트랜잭션은 읽기 트랜잭션에 의해 지연되지 않고 자유롭게 데이터를 수정할 수 있습니다.
### 2. MVCC 동작 방식 (개념 예시)
`Products` 테이블에 `inventory` 필드가 있다고 가정해 봅시다. MVCC 환경에서는 하나의 데이터가 업데이트될 때마다 새로운 버전이 생성됩니다.
**초기 상태 (트랜잭션 ID `1000`에서 커밋되었다고 가정):**
| id | name | inventory | tx\_start\_id | tx\_end\_id |
| :-- | :----- | :-------- | :------------ | :---------- |
| 1 | Laptop | 100 | 1000 | NULL |
**시나리오:**
1. **트랜잭션 A (읽기) 시작:** `tx_id = 1001`
* 트랜잭션 A는 `id = 1`인 `Laptop`의 `inventory`를 조회합니다.
* 자신의 스냅샷(`1001`) 기준으로 유효한 행을 찾습니다. (`tx_start_id <= 1001` 이고 `tx_end_id IS NULL OR tx_end_id > 1001` 조건을 만족)
* 결과: `inventory = 100`을 읽습니다.
2. **트랜잭션 B (쓰기) 시작:** `tx_id = 1002`
* 트랜잭션 B는 `id = 1`인 `Laptop`의 `inventory`를 `90`으로 업데이트합니다.
* **기존 버전 종료:** `id = 1`인 기존 행의 `tx_end_id`를 `1002`로 설정합니다.
* **새 버전 생성:** `id = 1`인 새 행을 `inventory = 90`, `tx_start_id = 1002`, `tx_end_id = NULL`로 삽입합니다.
**업데이트 후 상태 (트랜잭션 B 커밋 전):**
| id | name | inventory | tx\_start\_id | tx\_end\_id |
| :-- | :----- | :-------- | :------------ | :---------- |
| 1 | Laptop | 100 | 1000 | 1002 |
| 1 | Laptop | 90 | 1002 | NULL |
**계속되는 동작:**
* **트랜잭션 A (읽기):** 여전히 자신의 스냅샷(`1001`) 기준으로 데이터를 조회하므로, `tx_start_id <= 1001 AND (tx_end_id IS NULL OR tx_end_id > 1001)` 조건에 따라 첫 번째 행을 선택하여 `inventory = 100`을 계속 읽습니다. 트랜잭션 B의 변경사항에 전혀 영향을 받지 않습니다.
* **트랜잭션 C (새로운 읽기) 시작:** `tx_id = 1003` (트랜잭션 B가 커밋된 후)
* 트랜잭션 C는 `id = 1`인 `Laptop`의 `inventory`를 조회합니다.
* `tx_start_id <= 1003 AND (tx_end_id IS NULL OR tx_end_id > 1003)` 조건에 따라 두 번째 행을 선택하여 `inventory = 90`을 읽습니다. 트랜잭션 B의 최신 변경사항을 보게 됩니다.
이처럼 MVCC는 여러 버전의 데이터를 동시에 유지하며, 각 트랜잭션이 자신의 시작 시점 스냅샷에 해당하는 데이터를 일관성 있게 볼 수 있도록 합니다.
### 3. MVCC의 장점
* **높은 동시성:** 읽기와 쓰기 작업이 서로를 블록하지 않아 동시 처리량이 크게 향상됩니다. 특히 읽기 작업이 많은 시스템에서 효과적입니다.
* **일관된 읽기:** 트랜잭션은 자신이 시작된 시점의 스냅샷을 기반으로 데이터를 읽으므로, 트랜잭션 진행 중에 다른 트랜잭션의 변경 사항으로 인해 데이터 불일치(dirty read, non-repeatable read)가 발생하는 것을 방지합니다.
* **데드락 감소:** 읽기 트랜잭션이 잠금을 사용하지 않으므로, 잠금으로 인한 교착 상태 발생 위험이 줄어듭니다.
### 4. MVCC의 고려사항
* **저장 공간 오버헤드:** 데이터의 여러 버전을 유지해야 하므로, 기존 데이터를 덮어쓰는 방식보다 더 많은 저장 공간이 필요합니다.
* **가비지 컬렉션(Garbage Collection):** 더 이상 어떤 트랜잭션도 참조하지 않는 오래된 데이터 버전들은 주기적으로 정리(VACUUM 등)되어야 합니다. 이 과정 또한 시스템 자원을 소모할 수 있습니다.
## 결론: 현대 데이터베이스의 필수 요소, MVCC
MVCC는 현대 관계형 데이터베이스(PostgreSQL, MySQL InnoDB, Oracle, SQL Server의 Snapshot Isolation 등)에서 고성능과 높은 신뢰성을 동시에 달성하기 위한 핵심 기술입니다. 데이터베이스가 복잡한 동시성 요구사항을 처리하면서도 데이터 무결성을 굳건히 지킬 수 있도록 돕는 MVCC는 오늘날 우리가 빠르고 안정적으로 데이터를 이용할 수 있게 하는 숨은 마법사라고 할 수 있습니다.
MVCC의 이해는 데이터베이스 시스템의 동작 원리와 성능 최적화 전략을 깊이 있게 파악하는 데 필수적인 요소이며, 동시성 문제에 직면했을 때 효과적인 해결책을 모색하는 데 큰 도움이 될 것입니다.
<p class = "placeholder">Text by Chaelin & Gemini. Photographs by Chaelin, Unsplash.</p>