목차

    PostgreSQL의 MVCC(Multi-Version Concurrency Control)는 데이터베이스의 동시성을 효율적으로 관리하는 핵심 기술입니다. 본 포스팅에서는 PostgreSQL의 MVCC 동작 원리를 격리 수준별로 상세하게 분석하고, 각 격리 수준이 데이터 일관성과 동시성에 미치는 영향을 심층적으로 파헤쳐 봅니다. 이를 통해 PostgreSQL 데이터베이스를 더욱 효과적으로 활용하고, 발생 가능한 문제점을 사전에 예방할 수 있도록 돕습니다.

    MVCC 개념 이해

    MVCC(Multi-Version Concurrency Control)는 여러 트랜잭션이 동시에 데이터베이스에 접근할 때, 각 트랜잭션에게 데이터의 특정 시점 스냅샷을 제공하여 읽기 작업과 쓰기 작업 간의 충돌을 최소화하는 동시성 제어 기술입니다. PostgreSQL은 MVCC를 통해 데이터베이스의 일관성을 유지하면서 높은 동시성을 확보합니다.

    MVCC의 핵심은 데이터 변경 시 기존 데이터를 덮어쓰는 대신, 새로운 버전의 데이터를 생성하는 것입니다. 각 트랜잭션은 시작 시점에 따라 특정 버전의 데이터를 읽게 되므로, 다른 트랜잭션의 변경 사항에 영향을 받지 않고 일관된 데이터를 볼 수 있습니다.

    트랜잭션 ID와 MVCC

    PostgreSQL은 트랜잭션 ID(Transaction ID, XID)를 사용하여 데이터 버전을 관리합니다. 각 트랜잭션은 고유한 XID를 할당받으며, 데이터 행에는 해당 행을 생성한 트랜잭션의 XID(xmin)와 행을 삭제한 트랜잭션의 XID(xmax)가 저장됩니다.

    트랜잭션이 데이터를 읽을 때, PostgreSQL은 해당 트랜잭션의 격리 수준과 XID를 기반으로 어떤 버전의 데이터를 보여줄지 결정합니다. 예를 들어, 현재 활성 상태인 트랜잭션이 생성한 행은 아직 커밋되지 않았으므로 다른 트랜잭션에게는 보이지 않습니다.

    격리 수준과 동작 방식

    PostgreSQL은 ANSI SQL 표준에서 정의된 격리 수준을 대부분 지원하며, 각 격리 수준은 동시성과 데이터 일관성 사이의 균형을 조절합니다. 주요 격리 수준은 다음과 같습니다.

    • Read Uncommitted (미지원): PostgreSQL은 Read Uncommitted 격리 수준을 지원하지 않습니다. Read Uncommitted로 설정하더라도 Read Committed 격리 수준으로 동작합니다.
    • Read Committed: 각 쿼리가 시작될 때마다 최신 커밋된 스냅샷을 사용합니다. 한 쿼리 내에서는 일관성을 보장하지만, 쿼리 실행 중에 다른 트랜잭션이 커밋하면 다른 결과를 반환할 수 있습니다 (Non-Repeatable Read 발생 가능).
    • Repeatable Read: 트랜잭션이 시작될 때의 스냅샷을 유지합니다. 트랜잭션 내에서는 항상 동일한 데이터를 보장하지만, 다른 트랜잭션의 변경 사항이 반영되지 않아 Phantom Read가 발생할 수 있습니다.
    • Serializable: 가장 강력한 격리 수준으로, 트랜잭션이 순차적으로 실행되는 것과 동일한 결과를 보장합니다. Serializable Isolation Failure를 통해 동시성 문제를 감지하고 트랜잭션을 롤백합니다.

    Read Committed 상세 분석

    Read Committed는 PostgreSQL의 기본 격리 수준입니다. 각 SQL 문장이 시작될 때마다 최신 커밋된 데이터의 스냅샷을 가져와 사용합니다. 이는 한 트랜잭션 내에서 여러 SQL 문장을 실행하더라도, 각 문장이 시작되는 시점에 커밋된 데이터만 참조한다는 의미입니다.

    장점: 동시성이 높고, 구현이 비교적 간단합니다.

    단점: Non-Repeatable Read 현상이 발생할 수 있습니다. 즉, 트랜잭션 내에서 동일한 SELECT 문을 여러 번 실행했을 때, 다른 트랜잭션의 커밋으로 인해 다른 결과를 얻을 수 있습니다.

    예시: 트랜잭션 A가 `SELECT balance FROM accounts WHERE id = 1;`를 실행하여 잔액을 확인합니다. 이후 트랜잭션 B가 해당 계좌의 잔액을 변경하고 커밋합니다. 트랜잭션 A가 다시 동일한 SELECT 문을 실행하면, 트랜잭션 B의 변경 사항이 반영된 새로운 잔액을 보게 됩니다.

    Repeatable Read 상세 분석

    Repeatable Read 격리 수준은 트랜잭션이 시작될 때의 데이터베이스 스냅샷을 사용하여 트랜잭션 내에서 일관된 데이터를 보장합니다. 즉, 트랜잭션 내에서 동일한 SELECT 문을 여러 번 실행하더라도 항상 동일한 결과를 얻을 수 있습니다.

    장점: Non-Repeatable Read 문제를 해결하고, 트랜잭션 내에서 데이터 일관성을 유지할 수 있습니다.

    단점: Phantom Read 현상이 발생할 수 있습니다. 즉, 트랜잭션 내에서 특정 조건을 만족하는 행을 SELECT한 후, 다른 트랜잭션이 해당 조건을 만족하는 새로운 행을 삽입하고 커밋하면, 이후 동일한 SELECT 문을 실행했을 때 새로운 행이 추가된 결과를 얻을 수 있습니다.

    예시: 트랜잭션 A가 `SELECT * FROM orders WHERE status = 'pending';`를 실행하여 보류 중인 주문 목록을 확인합니다. 이후 트랜잭션 B가 새로운 보류 중인 주문을 삽입하고 커밋합니다. 트랜잭션 A가 다시 동일한 SELECT 문을 실행하면, 트랜잭션 B가 삽입한 새로운 주문이 결과에 포함됩니다.

    Serializable 격리 수준

    Serializable 격리 수준은 가장 강력한 격리 수준으로, 트랜잭션이 다른 트랜잭션과 완전히 격리되어 순차적으로 실행되는 것과 동일한 결과를 보장합니다. 이를 위해 PostgreSQL은 Serializable Isolation Failure를 사용하여 동시성 문제를 감지하고 트랜잭션을 롤백합니다.

    장점: 데이터 일관성을 최대로 보장합니다.

    단점: 동시성이 낮고, Serializable Isolation Failure로 인해 트랜잭션 롤백이 발생할 수 있습니다.

    Serializable Isolation Failure 예시: 두 트랜잭션이 동시에 동일한 계좌의 잔액을 확인하고, 잔액을 증가시키는 작업을 수행하려 할 때 발생할 수 있습니다. PostgreSQL은 두 트랜잭션 중 하나를 롤백하여 데이터 일관성을 유지합니다.

    적용: 중요한 금융 거래나 데이터 무결성이 최우선시되는 환경에서 사용됩니다.

    격리 수준 선택 가이드

    PostgreSQL의 격리 수준은 애플리케이션의 요구 사항에 따라 적절하게 선택해야 합니다.

    • Read Committed: 대부분의 애플리케이션에 적합하며, 동시성이 중요한 경우에 사용됩니다.
    • Repeatable Read: 데이터 일관성이 더 중요한 경우에 사용되며, Non-Repeatable Read 문제를 방지해야 할 때 유용합니다.
    • Serializable: 데이터 무결성이 최우선시되는 경우에 사용되며, 금융 거래와 같이 중요한 데이터를 다루는 환경에 적합합니다.

    격리 수준을 선택할 때는 동시성과 데이터 일관성 사이의 균형을 신중하게 고려해야 합니다. 과도하게 높은 격리 수준은 동시성을 저하시키고, 불필요한 성능 저하를 초래할 수 있습니다.