1. MVCC란?
데이터베이스는 여러 사용자가 동시에 같은 데이터를 조회하고 수정하는 환경에서 동작한다. 이러한 환경에서는 데이터의
일관성(Consistency)을 유지하면서도 여러 요청을 동시에 처리할 수 있는 동시성(Concurrency)을 확보해야 한다.
데이터베이스가 동시 접근을 제어하는 대표적인 방식으로는 Lock 기반 동시성 제어(Lock-Based Concurrency Control)와
MVCC(Multi-Version Concurrency Control)가 있다.
Lock 기반 동시성 제어
Lock 기반 동시성 제어는 트랜잭션이 특정 데이터에 접근할 때 해당 데이터에 잠금을 설정하여 다른 트랜잭션의 접근을 제한하는 방식이다.
예를 들어 하나의 트랜잭션이 데이터를 수정하기 위해 쓰기 Lock을 획득하면, 동일한 데이터를 수정하려는 다른 트랜잭션은 기존 Lock이
해제될 때까지 기다려야 한다. 격리 수준이나 Lock의 종류에 따라서는 조회 작업 역시 수정 작업이 끝날 때까지 대기할 수 있다.
Lock은 동시에 실행되는 트랜잭션이 서로의 작업을 침범하지 않도록 하여 데이터의 일관성을 지켜준다. 그러나 동시 접근이 많은 환경에서는
다음과 같은 문제가 발생할 수 있다.
- 동일한 데이터에 접근하는 트랜잭션이 많을수록 대기 시간이 증가한다.
- Lock 경쟁이 심해지면 전체 시스템의 처리량이 낮아질 수 있다.
- 여러 트랜잭션이 서로 상대방의 Lock을 기다리면 Deadlock이 발생할 수 있다.
- 읽기 작업과 쓰기 작업이 서로를 차단하면 조회 성능이 저하될 수 있다.
따라서 Lock은 데이터 정합성을 유지하기 위해 반드시 필요한 기능이지만, 모든 읽기와 쓰기를 Lock만으로 제어할 경우 높은 동시성을
확보하기 어렵다.

MVCC(Multi-Version Concurrency Control)
MVCC는 하나의 데이터를 단일 상태로만 관리하지 않고, 여러 버전으로 관리하는 동시성 제어 방식이다.
데이터가 수정되더라도 기존 버전을 즉시 제거하지 않고 이전 버전과 새로운 버전을 일정 기간 함께 유지한다. 각 트랜잭션은 자신에게
허용된 시점의 데이터 버전을 선택하여 조회한다.
이 구조에서는 쓰기 작업이 새로운 데이터 버전을 만드는 동안, 읽기 작업은 기존에 커밋된 버전을 계속 조회할 수 있다. 따라서 일반적인
조회 작업이 수정 작업의 완료를 기다릴 필요가 줄어든다.
- 읽기와 쓰기가 서로를 차단하는 상황을 줄일 수 있다.
- Lock 대기와 Lock 경쟁을 감소시킬 수 있다.
- 여러 트랜잭션을 동시에 처리할 수 있어 처리량이 높아진다.
- 각 트랜잭션에 일관된 데이터 조회 결과를 제공할 수 있다.
다만 MVCC는 이전 데이터 버전을 저장하고 관리해야 하므로, 더 이상 사용되지 않는 버전을 정리하는 별도의 과정이 필요하다.
MVCC를 사용하는 데이터베이스
MVCC는 PostgreSQL만의 기술이 아니다. 여러 관계형 데이터베이스가 MVCC 또는 유사한 Row Versioning 방식을 사용한다.

MVCC의 목적은 비슷하지만, 이전 버전을 저장하는 위치와 가시성을 판단하는 방법은 데이터베이스마다 다르다.
MySQL InnoDB와 Oracle Database는 현재 데이터와 함께 Undo 영역에 변경 이전 정보를 저장하고, 필요한 경우 이를 이용해 과거 버전을
재구성한다. 반면 PostgreSQL은 수정 전 튜플을 테이블 내부에 남겨두고, 수정된 값을 가진 새로운 튜플을 생성한다. 이 글에서는 그중에서도
PostgreSQL의 MVCC 구현 방식을 중심으로 살펴본다.
2. PostgreSQL의 MVCC

PostgreSQL은 데이터를 수정할 때 기존 행을 제자리에서 덮어쓰지 않는다. 기존 행 버전을 남겨두고, 수정된 값을 가진 새로운 행 버전을 생성한다.
PostgreSQL에서 테이블에 물리적으로 저장되는 각각의 행 버전을 튜플(Tuple)이라고 한다. 애플리케이션에서는 하나의 행으로 보이더라도, 내부
적으로는 동일한 논리적 행에 대한 여러 튜플 버전이 존재할 수 있다.
PostgreSQL의 데이터 변경 작업은 다음과 같이 이해할 수 있다.
- INSERT는 새로운 튜플을 생성한다.
- UPDATE는 기존 튜플을 이전 버전으로 남겨두고, 수정된 값을 가진 새로운 튜플을 생성한다.
- DELETE는 튜플을 즉시 물리적으로 제거하지 않고, 더 이상 유효하지 않은 버전으로 표시한다.
따라서 하나의 행이 여러 번 수정되면, 해당 행의 이전 버전과 현재 버전이 테이블 페이지 안에 일정 기간 함께 존재하게 된다.
PostgreSQL은 현재 트랜잭션이 어떤 튜플을 볼 수 있는지 판단하기 위해 각 튜플에 트랜잭션 관련 정보를 저장한다. 대표적인 정보가
xmin, xmax, ctid와 같은 시스템 컬럼이다. PostgreSQL은 Snapshot과 튜플의 트랜잭션 상태를 비교해 해당 튜플의 가시성을 판단한다.
3. PostgreSQL은 UPDATE를 어떻게 처리할까?
일반적으로 UPDATE를 실행하면 기존 행의 값이 새로운 값으로 덮어써진다고 생각하기 쉽다. 그러나 PostgreSQL의 UPDATE는 제자리
수정 방식으로 동작하지 않는다.
예를 들어 다음 SQL이 실행되었다고 가정해 보자.

PostgreSQL은 기존 튜플의 balance 값을 직접 500으로 변경하지 않는다. 대신 기존 튜플을 이전 버전으로 표시하고, balance = 500인
새로운 튜플을 생성한다.

기존 튜플에는 자신을 대체한 트랜잭션의 정보가 기록되고, 새로운 튜플에는 자신을 생성한 트랜잭션의 정보가 기록된다.
이처럼 이전 버전과 새로운 버전이 함께 존재하기 때문에, 아직 변경 내용을 볼 수 없는 트랜잭션은 기존 튜플을 조회하고, 변경 내용을 볼 수
있는 트랜잭션은 새로운 튜플을 조회할 수 있다.

이를 흔히 DELETE + INSERT와 유사하다고 설명하지만, 실제 내부 동작이 일반적인 DELETE 문과 INSERT 문을 각각 실행하는 것과 완전히
동일한 것은 아니다. PostgreSQL은 튜플 버전 간 연결, 인덱스 처리, HOT Update 등의 최적화를 함께 수행한다.
트랜잭션 ID와 커밋 상태
PostgreSQL에서 튜플의 가시성을 판단하려면 해당 튜플을 생성하거나 종료한 트랜잭션의 상태를 알아야 한다.
- 이 튜플을 만든 트랜잭션이 커밋되었는가?
- 아직 실행 중인가?
- 롤백되었는가?
- 이 튜플을 삭제하거나 수정한 트랜잭션은 현재 Snapshot에서 보이는가?
PostgreSQL은 필요한 트랜잭션에 트랜잭션 ID(Transaction ID, XID)를 부여하고, 각 트랜잭션의 커밋 또는 중단 상태를 pg_xact라는
내부 영역에 기록한다. pg_xact는 과거에 CLOG라고 불렸다.
PostgreSQL은 튜플의 xmin, xmax, 현재 Snapshot, 그리고 트랜잭션의 커밋 상태를 함께 확인하여 해당 튜플의 가시성을 결정한다.
PostgreSQL의 일반적인 XID는 32비트 값이므로 계속 증가하다가 약 42억 개를 사용하면 순환한다. 이를 Transaction ID Wraparound라고
한다.
XID가 아무런 처리 없이 순환하면 오래된 트랜잭션과 새로 생성된 트랜잭션의 선후 관계를 정상적으로 판단하기 어려워질 수 있다.
PostgreSQL은 이를 방지하기 위해 오래된 튜플의 트랜잭션 정보를 동결하는 Freeze 작업을 수행한다. 이 작업은 VACUUM과
Autovacuum이 담당하는 중요한 역할 중 하나이다.
4. 숨겨진 시스템 컬럼 xmin, xmax
PostgreSQL은 각 튜플 버전의 생성과 종료 상태를 관리하기 위해 시스템 컬럼을 저장한다. 그중 MVCC의 가시성 판단에 핵심적으로
사용되는 값이 xmin과 xmax이다.

다만 xmin 값만으로는 해당 튜플이 보이는지 판단할 수 없다. 기록된 트랜잭션이 커밋되었는지, 진행 중인지 또는 롤백되었는지를 함께
확인해야 한다.

5. Snapshot이란?

Snapshot은 현재 SQL 문 또는 트랜잭션이 어떤 트랜잭션의 변경 결과까지 볼 수 있는지를 나타내는 기준 정보이다.
단순히 특정 시각의 데이터 전체를 복사해 두는 것이 아니라, Snapshot이 만들어진 시점의 트랜잭션 상태를 기록하는 개념에 가깝다.
- 이미 완료된 트랜잭션
- 현재 실행 중인 트랜잭션
- 아직 시작되지 않은 것으로 판단해야 하는 트랜잭션
PostgreSQL은 Snapshot과 각 튜플의 xmin, xmax를 비교하여 현재 SQL 문에서 해당 튜플을 볼 수 있는지 판단한다.
예를 들어 튜플을 생성한 트랜잭션이 아직 커밋되지 않았다면 다른 트랜잭션에서는 해당 튜플을 볼 수 없다. 반대로 튜플을 삭제한 트랜잭션이
아직 현재 Snapshot에서 보이지 않는다면, 현재 트랜잭션은 삭제 이전의 튜플을 계속 조회할 수 있다.
Isolation Level에 따른 Snapshot 차이
Snapshot을 생성하고 유지하는 시점은 트랜잭션 격리 수준에 따라 달라진다.
READ COMMITTED
PostgreSQL의 기본 격리 수준이다.
READ COMMITTED에서는 트랜잭션 전체가 하나의 Snapshot을 공유하는 것이 아니라, 각 SQL 문이 시작될 때 새로운 Snapshot을 사용한다. 따라서 같은 트랜잭션 안에서도 두 SELECT 사이에 다른 트랜잭션이 데이터를 변경하고 커밋하면 두 번째 SELECT에서는 변경된 결과가 보일 수
있다.

REPEATABLE READ
REPEATABLE READ에서는 일반적으로 트랜잭션의 첫 번째 실제 쿼리가 실행될 때 획득한 Snapshot을 트랜잭션이 끝날 때까지 유지한다.
따라서 다른 트랜잭션이 중간에 데이터를 변경하고 커밋하더라도, 현재 트랜잭션은 처음 획득한 Snapshot을 기준으로 데이터를 계속 조회한다.

6. MVCC의 핵심은 Visibility(가시성) 판단이다
지금까지 PostgreSQL의 MVCC를 구성하는 여러 요소를 살펴보았다.
- UPDATE는 기존 튜플을 직접 수정하지 않고 새로운 튜플 버전을 생성한다.
- 각 튜플에는 xmin, xmax와 같은 시스템 컬럼이 저장된다.
- Snapshot은 현재 SQL 문 또는 트랜잭션이 어떤 버전을 볼 수 있는지에 대한 기준을 제공한다.
- PostgreSQL은 pg_xact를 통해 각 트랜잭션의 커밋 또는 중단 상태를 확인한다.

하나의 논리적 행에 여러 개의 튜플 버전이 존재하더라도 PostgreSQL은 모든 버전을 반환하지 않는다. 현재 Snapshot을 기준으로
각 튜플의 xmin, xmax, 그리고 관련 트랜잭션의 커밋 상태를 함께 확인하여 현재 트랜잭션에서 조회 가능한 버전만 선택한다.

따라서 PostgreSQL의 MVCC는 단순히 여러 버전을 저장하는 기술이 아니다. 여러 버전 중 현재 트랜잭션에 올바른 버전을 선택해 제공함으로써,
읽기와 쓰기의 충돌을 줄이면서도 일관된 조회 결과를 유지하는 기술이라고 볼 수 있다.
마무리
PostgreSQL의 MVCC는 기존 데이터를 덮어쓰지 않고 새로운 튜플 버전을 생성하여 높은 동시성을 제공한다.
이 과정에서 xmin, xmax, Snapshot, 그리고 트랜잭션의 커밋 상태를 함께 활용해 현재 트랜잭션이 조회할 수 있는 데이터 버전을 결정한다.

이전 버전의 튜플이 계속 남아 있으면 저장 공간과 조회 성능에 영향을 줄 수 있다. 따라서 PostgreSQL은 VACUUM과 Autovacuum을 통해
더 이상 어떤 트랜잭션에서도 볼 수 없는 튜플을 정리하고, Transaction ID Wraparound를 방지하기 위한 Freeze 작업을 수행한다.
⭐발표자 : 정현진님
1. MVCC란?
데이터베이스는 여러 사용자가 동시에 같은 데이터를 조회하고 수정하는 환경에서 동작한다. 이러한 환경에서는 데이터의
일관성(Consistency)을 유지하면서도 여러 요청을 동시에 처리할 수 있는 동시성(Concurrency)을 확보해야 한다.
데이터베이스가 동시 접근을 제어하는 대표적인 방식으로는 Lock 기반 동시성 제어(Lock-Based Concurrency Control)와
MVCC(Multi-Version Concurrency Control)가 있다.
Lock 기반 동시성 제어
Lock 기반 동시성 제어는 트랜잭션이 특정 데이터에 접근할 때 해당 데이터에 잠금을 설정하여 다른 트랜잭션의 접근을 제한하는 방식이다.
예를 들어 하나의 트랜잭션이 데이터를 수정하기 위해 쓰기 Lock을 획득하면, 동일한 데이터를 수정하려는 다른 트랜잭션은 기존 Lock이
해제될 때까지 기다려야 한다. 격리 수준이나 Lock의 종류에 따라서는 조회 작업 역시 수정 작업이 끝날 때까지 대기할 수 있다.
Lock은 동시에 실행되는 트랜잭션이 서로의 작업을 침범하지 않도록 하여 데이터의 일관성을 지켜준다. 그러나 동시 접근이 많은 환경에서는
다음과 같은 문제가 발생할 수 있다.
따라서 Lock은 데이터 정합성을 유지하기 위해 반드시 필요한 기능이지만, 모든 읽기와 쓰기를 Lock만으로 제어할 경우 높은 동시성을
확보하기 어렵다.
MVCC(Multi-Version Concurrency Control)
MVCC는 하나의 데이터를 단일 상태로만 관리하지 않고, 여러 버전으로 관리하는 동시성 제어 방식이다.
데이터가 수정되더라도 기존 버전을 즉시 제거하지 않고 이전 버전과 새로운 버전을 일정 기간 함께 유지한다. 각 트랜잭션은 자신에게
허용된 시점의 데이터 버전을 선택하여 조회한다.
이 구조에서는 쓰기 작업이 새로운 데이터 버전을 만드는 동안, 읽기 작업은 기존에 커밋된 버전을 계속 조회할 수 있다. 따라서 일반적인
조회 작업이 수정 작업의 완료를 기다릴 필요가 줄어든다.
다만 MVCC는 이전 데이터 버전을 저장하고 관리해야 하므로, 더 이상 사용되지 않는 버전을 정리하는 별도의 과정이 필요하다.
MVCC를 사용하는 데이터베이스
MVCC는 PostgreSQL만의 기술이 아니다. 여러 관계형 데이터베이스가 MVCC 또는 유사한 Row Versioning 방식을 사용한다.
MVCC의 목적은 비슷하지만, 이전 버전을 저장하는 위치와 가시성을 판단하는 방법은 데이터베이스마다 다르다.
MySQL InnoDB와 Oracle Database는 현재 데이터와 함께 Undo 영역에 변경 이전 정보를 저장하고, 필요한 경우 이를 이용해 과거 버전을
재구성한다. 반면 PostgreSQL은 수정 전 튜플을 테이블 내부에 남겨두고, 수정된 값을 가진 새로운 튜플을 생성한다. 이 글에서는 그중에서도
PostgreSQL의 MVCC 구현 방식을 중심으로 살펴본다.
2. PostgreSQL의 MVCC
PostgreSQL은 데이터를 수정할 때 기존 행을 제자리에서 덮어쓰지 않는다. 기존 행 버전을 남겨두고, 수정된 값을 가진 새로운 행 버전을 생성한다.
PostgreSQL에서 테이블에 물리적으로 저장되는 각각의 행 버전을 튜플(Tuple)이라고 한다. 애플리케이션에서는 하나의 행으로 보이더라도, 내부
적으로는 동일한 논리적 행에 대한 여러 튜플 버전이 존재할 수 있다.
PostgreSQL의 데이터 변경 작업은 다음과 같이 이해할 수 있다.
따라서 하나의 행이 여러 번 수정되면, 해당 행의 이전 버전과 현재 버전이 테이블 페이지 안에 일정 기간 함께 존재하게 된다.
PostgreSQL은 현재 트랜잭션이 어떤 튜플을 볼 수 있는지 판단하기 위해 각 튜플에 트랜잭션 관련 정보를 저장한다. 대표적인 정보가
xmin, xmax, ctid와 같은 시스템 컬럼이다. PostgreSQL은 Snapshot과 튜플의 트랜잭션 상태를 비교해 해당 튜플의 가시성을 판단한다.
3. PostgreSQL은 UPDATE를 어떻게 처리할까?
일반적으로 UPDATE를 실행하면 기존 행의 값이 새로운 값으로 덮어써진다고 생각하기 쉽다. 그러나 PostgreSQL의 UPDATE는 제자리
수정 방식으로 동작하지 않는다.
예를 들어 다음 SQL이 실행되었다고 가정해 보자.
PostgreSQL은 기존 튜플의 balance 값을 직접 500으로 변경하지 않는다. 대신 기존 튜플을 이전 버전으로 표시하고, balance = 500인
새로운 튜플을 생성한다.
기존 튜플에는 자신을 대체한 트랜잭션의 정보가 기록되고, 새로운 튜플에는 자신을 생성한 트랜잭션의 정보가 기록된다.
이처럼 이전 버전과 새로운 버전이 함께 존재하기 때문에, 아직 변경 내용을 볼 수 없는 트랜잭션은 기존 튜플을 조회하고, 변경 내용을 볼 수
있는 트랜잭션은 새로운 튜플을 조회할 수 있다.
이를 흔히 DELETE + INSERT와 유사하다고 설명하지만, 실제 내부 동작이 일반적인 DELETE 문과 INSERT 문을 각각 실행하는 것과 완전히
동일한 것은 아니다. PostgreSQL은 튜플 버전 간 연결, 인덱스 처리, HOT Update 등의 최적화를 함께 수행한다.
트랜잭션 ID와 커밋 상태
PostgreSQL에서 튜플의 가시성을 판단하려면 해당 튜플을 생성하거나 종료한 트랜잭션의 상태를 알아야 한다.
PostgreSQL은 필요한 트랜잭션에 트랜잭션 ID(Transaction ID, XID)를 부여하고, 각 트랜잭션의 커밋 또는 중단 상태를 pg_xact라는
내부 영역에 기록한다. pg_xact는 과거에 CLOG라고 불렸다.
PostgreSQL은 튜플의 xmin, xmax, 현재 Snapshot, 그리고 트랜잭션의 커밋 상태를 함께 확인하여 해당 튜플의 가시성을 결정한다.
PostgreSQL의 일반적인 XID는 32비트 값이므로 계속 증가하다가 약 42억 개를 사용하면 순환한다. 이를 Transaction ID Wraparound라고
한다.
XID가 아무런 처리 없이 순환하면 오래된 트랜잭션과 새로 생성된 트랜잭션의 선후 관계를 정상적으로 판단하기 어려워질 수 있다.
PostgreSQL은 이를 방지하기 위해 오래된 튜플의 트랜잭션 정보를 동결하는 Freeze 작업을 수행한다. 이 작업은 VACUUM과
Autovacuum이 담당하는 중요한 역할 중 하나이다.
4. 숨겨진 시스템 컬럼 xmin, xmax
PostgreSQL은 각 튜플 버전의 생성과 종료 상태를 관리하기 위해 시스템 컬럼을 저장한다. 그중 MVCC의 가시성 판단에 핵심적으로
사용되는 값이 xmin과 xmax이다.
다만 xmin 값만으로는 해당 튜플이 보이는지 판단할 수 없다. 기록된 트랜잭션이 커밋되었는지, 진행 중인지 또는 롤백되었는지를 함께
확인해야 한다.
5. Snapshot이란?
Snapshot은 현재 SQL 문 또는 트랜잭션이 어떤 트랜잭션의 변경 결과까지 볼 수 있는지를 나타내는 기준 정보이다.
단순히 특정 시각의 데이터 전체를 복사해 두는 것이 아니라, Snapshot이 만들어진 시점의 트랜잭션 상태를 기록하는 개념에 가깝다.
PostgreSQL은 Snapshot과 각 튜플의 xmin, xmax를 비교하여 현재 SQL 문에서 해당 튜플을 볼 수 있는지 판단한다.
예를 들어 튜플을 생성한 트랜잭션이 아직 커밋되지 않았다면 다른 트랜잭션에서는 해당 튜플을 볼 수 없다. 반대로 튜플을 삭제한 트랜잭션이
아직 현재 Snapshot에서 보이지 않는다면, 현재 트랜잭션은 삭제 이전의 튜플을 계속 조회할 수 있다.
Isolation Level에 따른 Snapshot 차이
Snapshot을 생성하고 유지하는 시점은 트랜잭션 격리 수준에 따라 달라진다.
READ COMMITTED
PostgreSQL의 기본 격리 수준이다.
READ COMMITTED에서는 트랜잭션 전체가 하나의 Snapshot을 공유하는 것이 아니라, 각 SQL 문이 시작될 때 새로운 Snapshot을 사용한다. 따라서 같은 트랜잭션 안에서도 두 SELECT 사이에 다른 트랜잭션이 데이터를 변경하고 커밋하면 두 번째 SELECT에서는 변경된 결과가 보일 수
있다.
REPEATABLE READ
REPEATABLE READ에서는 일반적으로 트랜잭션의 첫 번째 실제 쿼리가 실행될 때 획득한 Snapshot을 트랜잭션이 끝날 때까지 유지한다.
따라서 다른 트랜잭션이 중간에 데이터를 변경하고 커밋하더라도, 현재 트랜잭션은 처음 획득한 Snapshot을 기준으로 데이터를 계속 조회한다.
6. MVCC의 핵심은 Visibility(가시성) 판단이다
지금까지 PostgreSQL의 MVCC를 구성하는 여러 요소를 살펴보았다.
하나의 논리적 행에 여러 개의 튜플 버전이 존재하더라도 PostgreSQL은 모든 버전을 반환하지 않는다. 현재 Snapshot을 기준으로
각 튜플의 xmin, xmax, 그리고 관련 트랜잭션의 커밋 상태를 함께 확인하여 현재 트랜잭션에서 조회 가능한 버전만 선택한다.
따라서 PostgreSQL의 MVCC는 단순히 여러 버전을 저장하는 기술이 아니다. 여러 버전 중 현재 트랜잭션에 올바른 버전을 선택해 제공함으로써,
읽기와 쓰기의 충돌을 줄이면서도 일관된 조회 결과를 유지하는 기술이라고 볼 수 있다.
마무리
PostgreSQL의 MVCC는 기존 데이터를 덮어쓰지 않고 새로운 튜플 버전을 생성하여 높은 동시성을 제공한다.
이 과정에서 xmin, xmax, Snapshot, 그리고 트랜잭션의 커밋 상태를 함께 활용해 현재 트랜잭션이 조회할 수 있는 데이터 버전을 결정한다.
이전 버전의 튜플이 계속 남아 있으면 저장 공간과 조회 성능에 영향을 줄 수 있다. 따라서 PostgreSQL은 VACUUM과 Autovacuum을 통해
더 이상 어떤 트랜잭션에서도 볼 수 없는 튜플을 정리하고, Transaction ID Wraparound를 방지하기 위한 Freeze 작업을 수행한다.
⭐발표자 : 정현진님