티스토리 뷰

트랜잭션 격리수준은 데이터베이스에서 동시성 제어를 어떻게 수행할 것인지 결정하는 핵심 요소입니다. 동일한 데이터에 여러 트랜잭션이 동시에 접근하는 상황에서, 어느 정도까지 서로의 작업을 보지 못하도록 할 것인지 정의하는 개념입니다. 격리수준은 단순한 설정 값이 아니라, 일관성과 성능 사이의 균형을 설계하는 기준이라고 이해하고 있습니다.

 

이번 글에서는 MySQL, Microsoft SQL Server, PostgreSQL의 기본 격리수준과 그 선택 배경, REPEATABLE READ에서 Phantom Read가 발생하는 구조적 이유, 그리고 SELECT FOR UPDATE가 필요한 상황을 중심으로 정리해 보겠습니다.

 


 

트랜잭션 격리수준의 기본 개념

 

ANSI SQL 표준에서는 격리수준을 통해 Dirty Read, Non-Repeatable Read, Phantom Read와 같은 동시성 이상 현상을 제어하도록 정의하고 있습니다. 격리수준이 낮을수록 동시성은 높아지지만 이상 현상이 발생할 가능성이 증가하며, 격리수준이 높을수록 일관성은 강화되지만 락 경합과 성능 비용이 증가합니다.

 

현대의 주요 RDBMS는 대부분 MVCC(Multi-Version Concurrency Control)를 기반으로 동작합니다. MVCC는 읽기 작업이 쓰기 작업과 충돌하지 않도록 과거 버전을 활용하는 방식입니다. 다만 각 데이터베이스는 MVCC를 구현하는 세부 전략이 다르며, 그 차이가 기본 격리수준 선택과도 연결됩니다.

 

이 관점은 『데이터 중심 애플리케이션 설계』에서 설명하는 트랜잭션 모델의 분석과도 일치한다고 이해하고 있습니다.

 


 

MySQL, MSSQL, PostgreSQL의 기본 격리수준과 선택 배경

 

 

MySQL

 

MySQL의 InnoDB 스토리지 엔진은 기본 격리수준으로 REPEATABLE READ를 사용합니다. 이는 스냅샷 기반 읽기를 통해 트랜잭션 시작 시점의 일관된 데이터를 제공하는 방향으로 설계되어 있습니다. 실무에서 반복 조회가 많은 환경에서 예측 가능한 읽기 결과를 제공하려는 선택으로 이해됩니다.

 

InnoDB는 MVCC와 함께 필요 시 next-key lock을 활용하여 범위 잠금을 지원합니다. 따라서 단순한 스냅샷 읽기뿐 아니라, 특정 상황에서는 더 강한 보호 메커니즘도 제공합니다.

 

 

Microsoft SQL Server

 

Microsoft SQL Server의 기본 격리수준은 READ COMMITTED입니다. 전통적으로는 공유 락 기반 동시성 제어를 사용합니다. 이는 높은 호환성과 안정성을 유지하면서도 일반적인 웹 애플리케이션 환경에서 충분한 성능을 제공하기 위한 선택으로 이해됩니다.

 

또한 SQL Server는 READ COMMITTED SNAPSHOT 옵션을 통해 MVCC 기반 동작으로 전환할 수 있습니다. 이 설계는 기존 시스템과의 호환성을 유지하면서 점진적 개선이 가능하도록 한 구조라고 볼 수 있습니다.

 

 

PostgreSQL

 

PostgreSQL의 기본 격리수준은 READ COMMITTED입니다. PostgreSQL은 MVCC를 핵심 설계로 채택하고 있으며, 각 SQL 문 단위로 스냅샷을 생성합니다. 이를 통해 높은 동시성과 안정적인 읽기 성능을 제공합니다.

 

PostgreSQL은 SERIALIZABLE 수준에서도 추가적인 검증 기반 기법을 사용하여 일관성을 강화합니다. 이는 단순 락 증가 방식이 아닌, 충돌 검출 기반 접근이라는 점에서 특징적입니다.

 


 

REPEATABLE READ에서 Phantom Read가 발생하는 결정적 이유

 

Phantom Read는 동일한 조건으로 다시 조회했을 때, 이전에는 존재하지 않던 행이 결과에 포함되는 현상입니다.

 

REPEATABLE READ는 일반적으로 이미 읽은 행의 내용이 변경되지 않도록 보장합니다. 그러나 Phantom Read는 “행 단위” 문제가 아니라 “범위 조건”의 문제입니다. 즉, 특정 WHERE 조건에 해당하는 행 집합 전체의 안정성이 완전히 보장되지 않는 구현 방식에서 발생합니다.

 

핵심 원인은 다음과 같이 이해됩니다.

 

  • REPEATABLE READ는 기본적으로 개별 행의 버전 일관성을 보장합니다.
  • 그러나 범위 조건에 대해 별도의 잠금 전략이 적용되지 않는 경우, 새로운 행의 INSERT를 완전히 차단하지 못할 수 있습니다.
  • 이 경우 동일 조건 재조회 시 결과 집합이 달라질 수 있습니다.

 

MVCC 기반 구현에서는 스냅샷을 통해 기존 행은 안정적으로 조회되지만, 범위 자체를 물리적으로 잠그지 않는 한 새로운 행의 유입은 막지 못할 수 있습니다. 따라서 Phantom Read의 발생 여부는 단순히 격리수준 이름이 아니라, 각 DBMS의 구현 전략에 따라 달라질 수 있다고 이해하고 있습니다.

 

이 차이는 이론적 정의와 실제 구현 간의 간극에서 비롯된다고 볼 수 있습니다.

 


 

SELECT FOR UPDATE를 사용해야 하는 경우

 

SELECT FOR UPDATE는 조회한 행에 대해 쓰기 잠금을 획득하는 구문입니다. 이는 읽기 이후 수정이 예정된 상황에서 경쟁 조건을 방지하기 위해 사용됩니다.

 

다음과 같은 경우에 사용이 적절하다고 판단됩니다.

 

  • 특정 자원을 조회한 후 즉시 업데이트해야 하는 경우
  • 재고 차감과 같이 동시성 충돌이 발생할 수 있는 로직
  • 동일 데이터에 대해 여러 트랜잭션이 동시에 수정하면 비즈니스 오류가 발생하는 경우
  • 읽기 후 계산 결과를 기반으로 상태를 변경하는 패턴

 

이 방식은 “읽고 판단한 뒤 수정하는 구조”에서 다른 트랜잭션의 개입을 차단하는 역할을 합니다. 단, 과도하게 사용하면 락 경합이 증가할 수 있으므로, 반드시 필요한 범위에 한해 사용하는 것이 일반적인 실무 원칙이라고 이해하고 있습니다.

 


 

실무적 관점에서의 격리수준 선택

 

격리수준은 단순히 높을수록 좋은 설정이 아닙니다. 시스템의 트래픽 특성, 데이터 정합성 요구 수준, 락 경합 가능성, 비즈니스 규칙의 엄격성 등을 종합적으로 고려해야 합니다.

 

대부분의 웹 서비스에서는 기본값을 유지하되, 특정 핵심 로직 구간에서만 명시적 락 또는 추가 제어를 사용하는 방식이 널리 사용됩니다. 이는 성능과 정합성 사이의 균형을 맞추기 위한 현실적인 전략이라고 생각합니다.

 

또한 SERIALIZABLE 수준은 가장 강력한 일관성을 제공하지만, 동시성 저하와 트랜잭션 충돌 증가 가능성이 있으므로 신중하게 적용해야 합니다.

 


 

개발자로서의 정리

 

트랜잭션 격리수준을 학습하면서 가장 중요하게 느낀 점은, “이름”이 아니라 “구현 방식”을 이해해야 한다는 점이었습니다. 동일한 REPEATABLE READ라 하더라도 각 데이터베이스의 동작 방식은 다를 수 있으며, MVCC와 락 전략의 조합에 따라 실제 보장 범위가 달라질 수 있습니다.

 

또한 Phantom Read의 개념은 단순 암기가 아니라, 범위 잠금과 스냅샷 격리의 차이를 이해해야 명확해진다고 느꼈습니다. SELECT FOR UPDATE 역시 단순한 문법이 아니라, 비즈니스 정합성을 보장하기 위한 도구로 이해하는 것이 중요하다고 생각합니다.

 

트랜잭션 격리수준은 결국 시스템 설계의 문제이며, 데이터 정합성 요구와 성능 요구 사이에서 균형을 찾는 과정이라고 정리하고 있습니다. 앞으로도 공식 문서를 기반으로 동작 원리를 확인하며, 구현 세부 사항을 정확히 이해하는 개발자로 성장하고자 합니다.