티스토리 뷰

데이터베이스의 트랜잭션 격리수준은 동시에 실행되는 여러 트랜잭션이 서로의 작업에 어느 정도 영향을 주고받을 수 있는지를 정의하는 핵심 개념입니다. 실무에서 격리수준은 단순한 설정값이 아니라, 내부적으로 어떤 잠금 전략과 버전 관리 방식을 사용하는지에 따라 시스템의 정합성과 성능 특성이 달라지는 중요한 요소입니다. 본 글에서는 MySQL, Microsoft SQL Server, PostgreSQL을 중심으로 REPEATABLE READ와 SERIALIZABLE의 내부 동작을 정리하고, MVCC와 Undo Log 기반의 격리 구현 원리를 함께 살펴보겠습니다.

 


 

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

 

격리수준은 Dirty Read, Non-repeatable Read, Phantom Read와 같은 동시성 문제를 제어하기 위해 존재합니다. ANSI SQL 표준은 이러한 이상 현상을 기준으로 격리수준을 정의하지만, 실제 DBMS 구현은 각 제품의 설계에 따라 차이가 있습니다.

 

특히 현대적인 RDBMS는 단순한 잠금 기반 모델뿐 아니라 MVCC(Multi-Version Concurrency Control)를 적극적으로 활용합니다. 이 구조는 읽기 작업과 쓰기 작업의 충돌을 줄이기 위한 설계이며, 버전 관리와 Undo Log를 통해 일관된 스냅샷을 제공합니다. 이러한 접근은 데이터 중심 애플리케이션 설계에서 강조되는 동시성 제어 방식이기도 합니다.

 


 

REPEATABLE READ에서 Phantom Read를 방지하는 방식

 

REPEATABLE READ는 동일 트랜잭션 내에서 동일한 쿼리를 반복 실행했을 때 항상 같은 결과를 보장하는 수준입니다. 그러나 Phantom Read를 방지하는 방식은 DBMS마다 다릅니다.

 

 

MySQL의 Next-key Lock과 Gap Lock

 

MySQL의 InnoDB 엔진은 REPEATABLE READ에서 범위 잠금을 적극적으로 활용합니다. 특히 인덱스를 기준으로 동작하며, 다음과 같은 잠금 전략을 사용합니다.

 

Next-key Lock은 특정 인덱스 레코드에 대한 Record Lock과, 그 이전 범위에 대한 Gap Lock을 결합한 구조입니다. 이 방식은 단순히 기존 행을 보호하는 것이 아니라, 해당 범위에 새로운 행이 삽입되는 것을 차단합니다. 결과적으로 동일한 조건으로 범위 조회를 수행하더라도 새로운 레코드가 끼어드는 상황을 방지할 수 있습니다.

 

Gap Lock은 인덱스 상의 레코드와 레코드 사이의 공간을 잠급니다. 이 잠금은 Phantom Read를 방지하기 위한 핵심 요소이며, 범위 기반 쿼리에서 특히 중요한 역할을 합니다. 이러한 설계는 공식 InnoDB 잠금 모델에 기반합니다.

 

 

PostgreSQL의 Snapshot Isolation

 

PostgreSQL은 기본적으로 MVCC 기반의 Snapshot Isolation을 사용합니다. 트랜잭션이 시작될 때의 스냅샷을 기준으로 읽기를 수행하며, 이후 다른 트랜잭션이 커밋한 변경 사항은 현재 트랜잭션에 영향을 주지 않습니다.

 

이 구조에서는 읽기 작업이 잠금을 거의 사용하지 않습니다. 대신 각 행은 버전 정보를 가지며, 이전 버전은 시스템 내부에 유지됩니다. 이러한 버전 관리는 Undo 정보와 유사한 구조를 통해 구현됩니다. Snapshot Isolation은 읽기 일관성을 제공하지만, 특정 조건에서는 write skew와 같은 특성이 나타날 수 있으므로, SERIALIZABLE과는 다른 보장 범위를 가집니다.

 

 

Microsoft SQL Server의 접근 방식

 

SQL Server는 기본적으로 잠금 기반 모델을 사용합니다. REPEATABLE READ에서는 읽은 데이터에 대해 공유 잠금을 유지하여 다른 트랜잭션의 변경을 차단합니다. 그러나 Phantom Read 방지는 일반적으로 SERIALIZABLE 수준에서 범위 잠금을 통해 보장됩니다.

 


 

SERIALIZABLE에서 성능 저하가 발생하는 원리

 

SERIALIZABLE은 동시 실행 결과가 어떤 직렬 실행과 동일해야 한다는 가장 강한 격리수준입니다. 이를 달성하기 위해 DBMS는 더 넓은 범위의 잠금 전략을 사용합니다.

 

 

공유 잠금과 잠금 유지 시간

 

SERIALIZABLE에서는 읽기 작업에도 공유 잠금이 적용되며, 단순 조회가 트랜잭션 종료 시점까지 영향을 받을 수 있습니다. 이는 다른 트랜잭션의 쓰기 작업을 차단할 가능성을 높입니다.

 

공유 잠금이 증가하면 동시에 접근하는 트랜잭션 간의 대기 시간이 늘어나고, 시스템 전체의 처리량이 감소합니다. 특히 읽기와 쓰기가 혼합된 환경에서는 잠금 경합이 빈번해질 수 있습니다.

 

 

잠금 경합과 범위 확장

 

SERIALIZABLE은 단일 레코드가 아니라 범위 단위로 잠금을 확장하는 경우가 많습니다. 이로 인해 동일 인덱스 범위를 여러 트랜잭션이 동시에 접근할 때 충돌 가능성이 높아집니다. 잠금 경합이 증가하면 컨텍스트 스위칭과 대기 시간이 증가하고, 결과적으로 응답 시간이 악화됩니다.

 

이러한 특성은 정합성을 극대화하는 대신 성능 비용을 지불하는 구조라고 볼 수 있습니다.

 


 

정산과 대사 도메인에서의 SERIALIZABLE 활용과 최적화

 

정산 및 대사 시스템은 데이터 정합성이 매우 중요한 영역입니다. 중복 정산, 누락 정산, 금액 불일치는 심각한 비즈니스 문제로 이어질 수 있으므로 강한 격리수준이 요구되는 경우가 있습니다.

 

SERIALIZABLE을 사용하면서 성능을 최적화하기 위해 다음과 같은 접근이 활용됩니다.

 

첫째, 트랜잭션 범위를 최소화합니다. 불필요한 조회나 외부 호출을 트랜잭션 내부에서 수행하지 않도록 설계하여 잠금 유지 시간을 줄입니다.

 

둘째, 인덱스를 정교하게 설계합니다. 적절한 인덱스가 없는 범위 조회는 더 넓은 잠금으로 이어질 수 있습니다. 인덱스 최적화는 잠금 범위를 줄이는 핵심 요소입니다.

 

셋째, 배치 단위 처리를 고려합니다. 대량 처리 시 작은 단위로 커밋하여 잠금 점유 시간을 줄이면 전체 시스템의 동시성을 개선할 수 있습니다.

 

넷째, 충돌 가능성이 높은 로직은 애플리케이션 레벨에서 직렬화하거나 큐 기반으로 처리하는 방식을 고려할 수 있습니다. 이는 DBMS 내부 잠금에 의존하는 대신 비즈니스 레벨에서 순서를 통제하는 전략입니다.

 

이러한 방법은 강한 격리 수준을 유지하면서도 실제 운영 환경에서의 병목을 완화하는 실무적 접근입니다.

 


 

MVCC와 Undo Log 기반의 Lock 없는 격리

 

현대 RDBMS는 읽기 성능을 개선하기 위해 MVCC를 적극 활용합니다. MVCC는 데이터의 여러 버전을 동시에 유지하는 구조입니다.

 

쓰기 작업이 발생하면 기존 데이터를 즉시 덮어쓰지 않고, 새로운 버전을 생성합니다. 이전 버전은 Undo Log에 저장됩니다. 읽기 작업은 특정 시점의 스냅샷을 기준으로 적절한 버전을 선택합니다.

 

이 구조의 장점은 읽기와 쓰기 간의 충돌을 줄일 수 있다는 점입니다. 읽기 작업은 대부분 잠금을 사용하지 않으며, 높은 동시성을 제공합니다. PostgreSQL과 MySQL(InnoDB)은 이러한 구조를 기반으로 설계되어 있습니다.

 

Undo Log는 단순한 복구 메커니즘을 넘어, 과거 버전을 재구성하는 핵심 데이터 구조입니다. 이를 통해 트랜잭션 시작 시점의 일관된 상태를 유지할 수 있습니다. 이러한 설계는 데이터 중심 애플리케이션에서 강조되는 일관성과 확장성을 동시에 고려한 결과라고 볼 수 있습니다.

 


 

정리하며

 

트랜잭션 격리수준은 단순한 설정 옵션이 아니라, DBMS 내부의 잠금 전략과 버전 관리 방식이 결합된 결과물입니다. REPEATABLE READ에서의 Next-key Lock과 Gap Lock, Snapshot Isolation의 동작 방식은 각 제품의 철학을 보여주는 사례라고 생각합니다. SERIALIZABLE은 강한 정합성을 제공하지만, 공유 잠금과 범위 잠금으로 인해 성능 비용이 발생할 수 있다는 점도 확인할 수 있습니다.

 

MVCC와 Undo Log 기반 설계는 현대 RDBMS가 동시성과 정합성을 균형 있게 달성하기 위한 중요한 접근이라고 이해하게 되었습니다. 이러한 내부 원리를 정확히 이해하는 과정은 단순한 설정 사용을 넘어, 시스템 설계 관점에서 데이터베이스를 바라보는 데 큰 도움이 되었습니다. 개발자로서 이러한 개념을 정리해보는 과정 자체가 아키텍처 이해도를 높이는 의미 있는 경험이라고 생각합니다.