MySQL 트랜잭션 격리 수준(Isolation Level) 완벽 정리

트랜잭션 격리 수준이란?

트랜잭션 격리 수준(Transaction Isolation Level)은 여러 트랜잭션이 동시에 실행될 때, 각 트랜잭션이 다른 트랜잭션의 변경 사항을 어느 정도까지 관찰할 수 있는지 정의하는 설정이다.

데이터베이스에서는 여러 사용자가 동시에 데이터를 조회하고 수정할 수 있다. 이때 한 트랜잭션이 데이터를 변경하는 도중 다른 트랜잭션이 해당 데이터를 읽거나 수정하면 예상하지 못한 결과가 발생할 수 있다.

예를 들어 온라인 게임 서버에서 두 사용자가 동시에 같은 계정의 포인트를 조회하고 차감한다고 가정하자. 두 트랜잭션이 서로의 변경 사항을 올바르게 처리하지 못하면 포인트가 잘못 계산되거나 데이터 정합성이 깨질 수 있다.

트랜잭션 격리 수준은 이러한 동시성 문제를 제어하는 핵심 요소다.

격리 수준이 높아질수록 일반적으로 데이터의 일관성과 격리성이 강화되지만, 잠금 경합과 대기 시간이 증가할 수 있다. 따라서 무조건 가장 높은 격리 수준을 선택하는 것이 아니라, 데이터 정합성과 동시 처리 성능 사이에서 적절한 균형을 찾아야 한다.

MySQL의 InnoDB 스토리지 엔진은 다음 네 가지 격리 수준을 지원하며, 기본값은 REPEATABLE READ다.

트랜잭션 격리 수준의 핵심 문제

격리 수준을 이해하려면 동시 실행 환경에서 발생하는 대표적인 읽기 이상(Read Anomaly)을 알아야 한다.

Dirty Read

아직 커밋되지 않은 다른 트랜잭션의 변경 데이터를 읽는 현상이다.

트랜잭션 A가 포인트를 변경했지만 아직 커밋하지 않은 상태에서 트랜잭션 B가 변경된 값을 읽을 수 있다. 이후 트랜잭션 A가 롤백하면 트랜잭션 B는 최종적으로 존재하지 않는 데이터를 읽은 셈이 된다.

Non-repeatable Read

하나의 트랜잭션에서 동일한 행을 여러 번 조회했을 때 결과가 달라지는 현상이다.

트랜잭션 A가 포인트를 조회한 뒤, 트랜잭션 B가 해당 포인트를 수정하고 커밋한다. 이후 트랜잭션 A가 같은 행을 다시 조회하면 이전과 다른 값을 읽을 수 있다.

Phantom Read

하나의 트랜잭션에서 동일한 조건으로 조회했을 때, 다른 트랜잭션의 변경으로 조회 결과에 새로운 행이 나타나거나 기존 행이 사라지는 현상이다.

예를 들어 트랜잭션 A가 포인트가 1,000 이상인 사용자를 조회한다. 이후 트랜잭션 B가 조건에 해당하는 새로운 사용자를 추가하고 커밋한다. 트랜잭션 A가 같은 조건으로 다시 조회하면 결과 행의 개수가 달라질 수 있다.

Non-repeatable Read가 주로 동일한 행의 값이 달라지는 문제라면, Phantom Read는 조건에 해당하는 행의 집합이 달라지는 문제라는 차이가 있다.

Read Uncommitted

READ UNCOMMITTED는 네 가지 격리 수준 중 가장 낮은 수준이다.

다른 트랜잭션이 변경한 데이터가 아직 커밋되지 않았더라도 해당 데이터를 읽을 수 있다. 따라서 Dirty Read가 발생할 수 있다.

동작 방식

트랜잭션 A가 포인트를 1,000에서 500으로 변경했지만 아직 커밋하지 않았다고 가정하자.

이때 트랜잭션 B가 해당 포인트를 조회하면 500이라는 값을 읽을 수 있다.

이후 트랜잭션 A가 롤백하면 실제 포인트는 다시 1,000이 된다. 그러나 트랜잭션 B는 커밋되지 않은 500이라는 값을 이미 읽었다.

이것이 Dirty Read다.

특징

  • 커밋되지 않은 변경 데이터까지 읽을 수 있다.
  • Dirty Read, Non-repeatable Read, Phantom Read가 발생할 수 있다.
  • 일반적으로 읽기 작업의 격리성이 가장 낮다.
  • 데이터 정합성이 중요한 서비스에서는 사용을 피하는 편이 좋다.

활용 시 고려 사항

정확한 데이터가 중요하지 않은 일부 분석 작업에서도 결과가 왜곡될 수 있으므로 신중하게 사용해야 한다. 성능 향상을 목적으로 선택하더라도 실제 실행 계획과 동시성 특성을 측정해야 한다.

온라인 게임의 포인트, 결제 금액, 보상 지급, 정산 데이터처럼 정확성이 중요한 영역에는 적합하지 않다.

Read Committed

READ COMMITTED는 다른 트랜잭션이 커밋한 데이터만 읽을 수 있도록 하는 격리 수준이다.

Dirty Read를 방지하지만, 동일한 트랜잭션 안에서도 같은 데이터를 다시 조회하면 결과가 달라질 수 있다.

동작 방식

트랜잭션 A가 포인트를 조회해 1,000이라는 값을 확인했다고 가정하자.

이후 트랜잭션 B가 포인트를 500으로 변경하고 커밋한다.

트랜잭션 A가 같은 포인트를 다시 조회하면 500이라는 값을 읽을 수 있다. 두 조회 사이에 트랜잭션 B의 변경 사항이 커밋되었기 때문이다.

이처럼 커밋된 데이터만 읽더라도 Non-repeatable Read는 발생할 수 있다.

특징

  • 커밋되지 않은 데이터는 읽지 않는다.
  • Dirty Read를 방지한다.
  • Non-repeatable Read와 Phantom Read가 발생할 수 있다.
  • InnoDB에서는 일반적인 일관 읽기마다 새로운 스냅샷을 사용한다.
  • 일부 상황에서 잠금 경합을 줄이고 동시성을 높이는 데 유리할 수 있다.

활용 시 고려 사항

조회할 때마다 최신 커밋 데이터를 확인하는 특성이 필요한 서비스에서 고려할 수 있다.

다만 여러 번의 조회 결과가 동일해야 하는 비즈니스 로직이라면, 격리 수준만 믿기보다 트랜잭션 설계와 잠금 전략을 함께 검토해야 한다.

Repeatable Read

REPEATABLE READ는 동일한 트랜잭션에서 일반적인 조회를 반복하더라도 일관된 스냅샷을 읽을 수 있도록 하는 격리 수준이다.

MySQL InnoDB의 기본 격리 수준이다.

동작 방식

트랜잭션 A가 포인트를 조회해 1,000이라는 값을 확인했다고 가정하자.

이후 트랜잭션 B가 포인트를 500으로 변경하고 커밋한다.

트랜잭션 A가 같은 트랜잭션 안에서 일반적인 SELECT를 다시 실행하면 기존 스냅샷에 따라 1,000이라는 값을 계속 읽는다.

트랜잭션 A가 시작한 조회 관점에서 데이터의 일관성이 유지되는 것이다.

InnoDB는 MVCC(Multi-Version Concurrency Control)를 활용해 이러한 일관 읽기를 지원한다. 데이터가 변경되더라도 필요한 경우 이전 버전의 데이터를 읽을 수 있다.

특징

  • 일반적인 일관 읽기에서 Dirty Read를 방지한다.
  • 동일한 트랜잭션 내에서 반복 조회 결과를 일관되게 유지한다.
  • 일반적인 일관 읽기에서 Non-repeatable Read를 방지한다.
  • 조건 검색에 대한 Phantom Read 문제도 InnoDB의 잠금 읽기 및 쓰기에서는 갭 잠금(Gap Lock), 넥스트 키 잠금(Next-Key Lock) 등의 메커니즘으로 제어할 수 있다.
  • MVCC와 잠금 메커니즘을 함께 사용한다.

주의할 점: 일반 조회와 잠금 조회는 다르다

REPEATABLE READ라고 해서 트랜잭션 안의 모든 SQL이 언제나 동일한 데이터 상태를 관찰하는 것은 아니다.

일반적인 SELECT는 스냅샷을 기준으로 읽지만, SELECT ... FOR UPDATE 같은 잠금 읽기는 최신 데이터 상태를 기준으로 동작할 수 있다.

따라서 일반 조회와 잠금 조회를 혼합하면 서로 다른 시점의 데이터가 관찰될 수 있다. 중요한 비즈니스 로직에서는 이 차이를 고려해야 한다.

활용 시 고려 사항

일반적인 웹 서비스와 온라인 게임 서버에서 데이터 정합성과 동시 처리 성능의 균형을 맞추는 기본 선택지로 사용할 수 있다.

다만 격리 수준이 높다고 해서 모든 동시성 문제가 자동으로 해결되는 것은 아니다. 포인트 차감이나 보상 지급처럼 중복 실행이 위험한 로직은 조건부 UPDATE, 고유 제약 조건, 명시적 잠금, 멱등성 설계 등을 함께 적용해야 한다.

Serializable

SERIALIZABLE은 네 가지 격리 수준 중 가장 강한 격리 수준이다.

동시에 실행되는 트랜잭션들이 마치 순차적으로 실행된 것과 같은 결과를 얻도록 동시 접근을 엄격하게 제어한다.

동작 방식

트랜잭션 A가 특정 조건에 해당하는 데이터를 조회하고 있다고 가정하자.

트랜잭션 B가 같은 데이터 범위에 영향을 주는 INSERT, UPDATE 또는 DELETE 작업을 수행하려고 하면, 잠금 상황에 따라 트랜잭션 A가 종료될 때까지 대기할 수 있다.

MySQL InnoDB에서는 명시적인 트랜잭션이 활성화된 상태에서 일반적인 SELECT도 공유 잠금 읽기로 처리하는 방식 등을 통해 격리성을 강화한다.

단, 실제 잠금 범위와 대기 여부는 인덱스, SQL 문장, 트랜잭션 상태 등에 따라 달라진다.

특징

  • Dirty Read, Non-repeatable Read, Phantom Read를 방지하도록 동시 실행을 엄격하게 제어한다.
  • 충돌하는 읽기와 쓰기 사이에 잠금 대기가 발생할 수 있다.
  • 동시성이 감소하고 처리 지연이 커질 수 있다.
  • 잠금 경합과 데드락 가능성을 고려해야 한다.

활용 시 고려 사항

데이터 일관성에 대한 요구가 매우 높고 동시성 저하를 감수할 수 있는 특정 작업에서 고려할 수 있다.

그러나 SERIALIZABLE을 적용한다고 해서 애플리케이션의 모든 비즈니스 규칙이 자동으로 보장되는 것은 아니다. 트랜잭션 범위, 제약 조건, 외부 시스템과의 연동까지 별도로 설계해야 한다.

격리 수준 비교

격리 수준Dirty ReadNon-repeatable ReadPhantom Read
Read Uncommitted발생 가능발생 가능발생 가능
Read Committed방지발생 가능발생 가능
Repeatable Read방지방지일반적인 InnoDB 동작에서 잠금 방식에 따라 제어
Serializable방지방지방지

위 표는 일반적인 SQL 격리 수준의 개념을 기준으로 한다. 특히 MySQL InnoDB의 REPEATABLE READ는 MVCC 기반 일반 조회와 잠금 읽기의 동작이 다르므로, Phantom Read를 단순히 발생 또는 방지로만 외우기보다 실제 SQL 실행 방식까지 이해해야 한다.

MySQL에서 격리 수준 설정하기

세션의 격리 수준은 다음과 같이 변경할 수 있다.

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

해당 설정은 현재 세션에 적용된다.

현재 설정된 격리 수준은 다음과 같이 확인할 수 있다.

SELECT @@transaction_isolation;

서버의 기본 격리 수준을 변경하려면 다음과 같이 설정할 수 있다.

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

전역 설정은 일반적으로 이후 생성되는 세션의 기본값에 영향을 준다. 이미 연결된 세션에 즉시 동일하게 적용되는 것은 아니다. 운영 환경에서는 커넥션 풀을 포함한 실제 연결 구조를 고려해야 한다.

온라인 게임 서버에서의 활용

온라인 게임 서버에서는 모든 데이터에 동일한 격리 수준을 적용하기보다 데이터의 특성과 비즈니스 규칙에 맞게 동시성 제어 방식을 설계하는 것이 중요하다.

포인트 차감

포인트를 차감할 때는 잔액이 충분한지 확인하는 것과 실제 차감을 하나의 원자적인 작업으로 처리하는 것이 중요하다.

UPDATE users
SET points = points - 100
WHERE id = 1
  AND points >= 100;

영향받은 행의 수를 확인하면 차감 성공 여부를 판단할 수 있다. 다만 실제 서비스에서는 사용자 식별자, 차감 금액 검증, 트랜잭션 범위, 중복 요청 방지 등을 함께 처리해야 한다.

보상 지급과 정산

보상 지급이나 정산은 동일한 작업이 중복 실행되지 않도록 설계해야 한다.

  • 고유 제약 조건으로 중복 정산을 방지한다.
  • 관련 데이터 변경을 하나의 데이터베이스 트랜잭션으로 묶는다.
  • 충돌이 발생하면 실패를 감지하고 적절하게 재시도하거나 오류를 반환한다.
  • 외부 메시지 발행이 필요하다면 Transactional Outbox 등의 패턴을 검토한다.

격리 수준은 동시성 제어의 일부일 뿐이며, 중복 지급 방지나 분산 시스템의 일관성을 단독으로 보장하지는 않는다.

참고 자료

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다