콘텐츠로 이동

Isolation Level

격리 수준 (Isolation Level)은 여러 트랜잭션이 동시에 실행될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터에 접근하는 것을 어느 수준까지 허용할 것인지를 결정하는 설정이다.

격리 수준이 낮을수록 동시성은 높아지지만, 다양한 데이터 부정합 (Concurrency Issue) 문제가 발생할 가능성이 커진다.

  • 더티 리드 (Dirty Read): 한 트랜잭션이 아직 커밋 (Commit)되지 않은 다른 트랜잭션의 변경 데이터를 읽는 현상
    • 데이터를 변경한 트랜잭션이 롤백 (Rollback)되면, 데이터를 읽은 트랜잭션은 존재하지 않는 데이터를 참조하게 되어 데이터 불일치 문제가 발생
  • 반복 불가능한 읽기 (Non-Repeatable Read): 한 트랜잭션 내에서 동일한 조건으로 두 번 이상 SELECT 쿼리를 실행했을 때 각 조회 결과가 다르게 나타나는 현상
    • 그 사이에 다른 트랜잭션이 데이터를 수정하고 커밋하여 한 번 조회했던 레코드의 값이 중간에 변경되는 문제
  • 팬텀 리드 (Phantom Read): 한 트랜잭션 내에서 동일한 조건으로 첫 번째 조회에서는 없었던 새로운 레코드가 두 번째 조회에서 나타나는 현상
    • 다른 트랜잭션이 새로운 레코드를 추가하고 커밋하여 발생하는 문제

SQL 표준에서 정의하는 격리 수준은 네 가지이며, InnoDB 스토리지 엔진은 이 네 가지를 모두 지원한다.

격리 수준DIRTY READNON-REPEATABLE READPHANTOM READ
READ UNCOMMITTEDOOO
READ COMMITTEDXOO
REPEATABLE READXXO(InnoDB X)
SERIALIZABLEXXX

아래로 갈수록 격리 수준이 높아져 데이터 정합성은 강화되지만, 잠금의 범위가 넓어지고 길어져 동시 처리 성능은 저하된다.

가장 낮은 격리 수준으로, 한 트랜잭션의 변경 내용이 커밋이나 롤백 여부와 관계없이 다른 트랜잭션에 즉시 노출된다.

트랜잭션 1트랜잭션 2
BEGIN
INSERT OGU
SELECT OGU
정상 조회
COMMIT

오라클 DBMS에서 기본으로 사용되는 격리 수준으로, 온라인 서비스에서 가장 많이 사용되는 격리 수준이다.

  • DIRTY READ가 발생하지 않으며, 데이터를 변경했을 경우 COMMIT이 완료된 데이터만 다른 트랜잭션에서 조회 가능
  • 조회하려는 데이터가 COMMIT이 완료되지 않았다면, 캐시 (버퍼 풀)가 아닌 언두 (Undo) 영역에서 백업된 과거 레코드 조회
트랜잭션 1DATABASE트랜잭션 2
BEGINno: 59 name: DDUZY
UPDATE SET name = 'OGU' WHERE no = 59
SELECT WHERE no = 59
변경 전 데이터를 언두 로그로 복사DDUZY로 조회(언두 로그의 이전 데이터를 조회)
COMMITno: 59 name: OGU

이 격리 수준에서도 한 트랜잭션에서 동일한 데이터를 여러 번 조회하면, 각각의 조회는 동일한 결과를 반환하지 않을 수 있다. (NON-REPEATABLE READ 발생)

트랜잭션 1DATABASE트랜잭션 2
BEGIN
no: 59 name: OGUSELECT WHERE no = 59
OGU로 조회
BEGIN
UPDATE SET name = 'DDUZY' WHERE no = 59
COMMITno: 59 name: DDUZY
SELECT WHERE no = 59(같은 쿼리 실행)
DDUZY로 조회

일반적인 웹 애플리케이션에서는 문제가 없을 수 있으나, 하나의 트랜잭션에서 동일한 데이터를 여러 번 조회하고 변경하는 금전적인 거래와 같은 경우에는 문제가 발생할 수 있다.

InnoDB 스토리지 엔진에서 기본으로 사용되는 격리 수준으로, MVCC (Multi-Version Concurrency Control)를 이용한 방식이다.

  • 트랜잭션 내 첫 SELECT (일관된 읽기) 시점에 스냅샷을 생성하여, 이후 트랜잭션 내의 모든 SELECT 쿼리는 해당 스냅샷을 기준으로 데이터를 조회 (=MVCC)
  • 다른 트랜잭션이 데이터를 변경하고 커밋하더라도, 현재 트랜잭션은 자신의 트랜잭션 ID보다 낮은 (과거의) 번호를 가진 언두 로그 데이터를 끝까지 조회하여 정합성을 유지

MVCC (Multi Version Concurrency Control)를 이용한 Non-Repeatable Read 방지

섹션 제목: “MVCC (Multi Version Concurrency Control)를 이용한 Non-Repeatable Read 방지”

모든 InnoDB 트랜잭션에는 고유한 트랜잭션 ID (순차적으로 증가하는 값)가 부여되며, 레코드가 변경될 때 언두 영역에 백업된 과거 데이터에도 해당 트랜잭션 ID가 함께 저장된다.

트랜잭션 12DATABASE트랜잭션 10
TRX-ID: 6 no: 59 name: DDUZYBEGIN(TRX-ID: 10)
SELECT WHERE no = 59
DDUZY로 조회
BEGIN(TRX-ID: 12)
UPDATE SET name = 'OGU' WHERE no = 59
변경 전 데이터를 언두 로그로 복사
COMMIT(TRX-ID: 12)TRX-ID: 12 no: 59 name: OGU
TRX-ID: 6 no: 59 name: DDUZY
SELECT WHERE no = 59
자신의 트랜잭션 번호보다 작은 번호 중 최근 것 조회
DDUZY로 조회

트랜잭션 ID를 이용해 위 예시처럼 트랜잭션 12에서 데이터가 커밋되었더라도, 트랜잭션 10은 자신의 번호보다 작은 (이전에 시작된) 언두 로그의 데이터를 찾아 조회하게 되므로 NON-REPEATABLE READ 가 발생하지 않음.

MVCC (Multi Version Concurrency Control)와 Read View

섹션 제목: “MVCC (Multi Version Concurrency Control)와 Read View”

REPEATABLE READ는 트랜잭션 내 첫 조회 시점에 생성한 Read View (해당 시점에 활성 상태인 트랜잭션들의 ID 목록)를 사용하여 논리적인 스냅샷 읽기를 보장한다.

  • 조회 원리: 데이터를 읽을 때 레코드의 트랜잭션 ID (DB_TRX_ID)를 Read View와 비교하여 알맞은 버전의 데이터 조회
    • 자신의 트랜잭션 번호보다 크거나 현재 활성 중인 트랜잭션이 변경한 값이라면, 언두 로그를 통해 이전 버전의 데이터를 계속해서 탐색
  • 언두 로그 팽창 (Undo Log Bloat) 주의: 롱 트랜잭션 (Long Transaction)이 발생하면 해당 트랜잭션이 참조할 가능성이 있는 언두 로그를 시스템이 삭제하지 못하고 계속 보관해야 함
    • 이로 인해 언두 테이블스페이스 용량이 급격히 커지고 전반적인 시스템 성능 저하 (조회 탐색 비용 증가)를 유발할 수 있음

넥스트 키 락을 이용한 PHANTOM READ 방지

섹션 제목: “넥스트 키 락을 이용한 PHANTOM READ 방지”

위와 같이 잠금 없이 조회하는 일반적인 SELECT 쿼리는 MVCC를 통해 데이터 무결성을 보장하지만, FOR UPDATE처럼 배타적 잠금 (Write Lock)을 걸며 조회하는 경우에는 PHANTOM READ 문제가 발생할 수 있다.

일반적인 DBMS에서 배타적 잠금을 걸고 범위를 조회하게 되면, 트랜잭션 도중 새롭게 삽입된 데이터까지 읽어버려 PHANTOM READ가 발생한다.

트랜잭션 12DATABASE트랜잭션 10
TRX-ID: 6 no: 59 name: DDUZYBEGIN(TRX-10)
59인 레코드만 잠금SELECT WHERE no >= 59 FOR UPDATE
DDUZY 조회
BEGIN(TRX-ID: 12)
INSERT INTO t1 VALUES (60, 'OGU')
(대기 없이 바로 실행)
COMMIT(TRX-ID: 12)TRX-ID: 6 no: 59 name: DDUZY
TRX-ID: 12 no: 60: name: OGU
SELECT WHERE no >= 59 FOR UPDATE
DDUZY, OGU 조회
  • 일반 DBMS는 조건에 매칭된 id = 59 레코드 자체에만 잠금을 걸기 때문에, 트랜잭션 12의 id = 60 삽입 요청은 잠금 대기 없이 즉시 실행
  • 따라서 트랜잭션 10이 다시 조회할 때 없던 레코드 (60)가 나타나 PHANTOM READ가 발생

하지만 MySQL InnoDB 엔진에서는 넥스트 키 락 (Next-Key Lock = Record Lock + Gap Lock)의 존재로 문제를 방지할 수 있다.

  • 범위 검색 시 실제 존재하는 레코드뿐만 아니라 그 사이의 간격 (Gap)까지 잠금
  • 해당 구간에 새로운 데이터가 삽입되는 것 자체를 원천 차단
트랜잭션 12DATABASE트랜잭션 10
TRX-ID: 6 no: 59 name: DDUZYBEGIN(TRX-10)
59뿐만 아니라 59 이상의 레코드에 대해서도 잠금SELECT WHERE no >= 59 FOR UPDATE
DDUZY 조회
BEGIN(TRX-ID: 12)
INSERT INTO t1 VALUES (60, 'OGU')
넥스트 키 락 잠금 대기
SELECT WHERE no >= 59
DDUZY 조회
COMMIT(TRX-10)
COMMIT(TRX-ID: 12)TRX-ID: 6 no: 59 name: DDUZY
TRX-ID: 12 no: 60: name: OGU

갭 락 (Gap Lock)의 존재 덕분에 MySQL의 REPEATABLE READ에서는 Phantom Read가 발생하지 않는 것이 일반적이지만, 특수한 예외 시나리오에서는 발생할 수 있다.

  1. 첫 번째 조회는 잠금 없이 조회
  2. 두 번째 조회에서 베타적 잠금을 획득하여 조회

하지만 동일 트랜잭션 내에서 이렇게 비일관적인 읽기 방식을 섞어 쓰는 경우는 거의 없으므로, 발생하지 않는다고 볼 수 있다.

가장 단순하고 엄격한 격리 수준으로, 그만큼 동시 처리 성능이 다른 모든 격리 수준보다 심각하게 떨어진다.

  • 순수 읽기 작업 (SELECT)에도 암묵적으로 공유 잠금 (읽기 잠금)을 획득
  • 누군가 조회 중인 레코드는 다른 트랜잭션에서 절대 변경하거나 삭제할 수 없음

MySQL InnoDB 스토리지 엔진에서는 REPEATABLE READ 격리 수준에서도 넥스트 키 락을 통해 PHANTOM READ 문제를 방어할 수 있으므로, 거의 사용되지 않는다.

마지막 업데이트:

MySQL