Isolation Level(격리 수준)

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

트랜잭션 격리 수준의 필요성

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

  • 더티 리드 (Dirty Read): 한 트랜잭션이 아직 커밋(Commit)되지 않은 다른 트랜잭션의 변경 데이터를 읽는 현상

    • 데이터를 변경한 트랜잭션이 롤백(Rollback)되면, 데이터를 읽은 트랜잭션은 존재하지 않는 데이터를 참조하게 되어 데이터 불일치 문제가 발생

  • 반복 불가능한 읽기 (Non-Repeatable Read): 한 트랜잭션 내에서 동일한 조건으로 두 번 이상 SELECT 쿼리를 실행했을 때 각 조회 결과가 다르게 나타나는 현상

    • 그 사이에 다른 트랜잭션이 데이터를 수정하고 커밋하여 한 번 조회했던 레코드의 값이 중간에 변경되는 문제

  • 팬텀 리드 (Phantom Read): 한 트랜잭션 내에서 동일한 조건으로 첫 번째 조회에서는 없었던 새로운 레코드가 두 번째 조회에서 나타나는 현상

    • 다른 트랜잭션이 새로운 레코드를 추가하고 커밋하여 발생하는 문제

격리 수준의 종류

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

격리 수준
DIRTY READ
NON-REPEATABLE READ
PHANTOM READ

READ UNCOMMITTED

O

O

O

READ COMMITTED

X

O

O

REPEATABLE READ

X

X

O(InnoDB X)

SERIALIZABLE

X

X

X

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

READ UNCOMMITTED

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

트랜잭션 1
트랜잭션 2

BEGIN

INSERT OGU

SELECT OGU

정상 조회

COMMIT

READ COMMITTED

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

  • DIRTY READ가 발생하지 않으며, 데이터를 변경했을 경우 COMMIT이 완료된 데이터만 다른 트랜잭션에서 조회 가능

  • 조회하려는 데이터가 COMMIT이 완료되지 않았다면, 언두 영역에서 백업된 레코드 조회

트랜잭션 1
DATABASE
트랜잭션 2

BEGIN

no: 59 name: DDUZY

UPDATE SET name = 'OGU' WHERE no = 59

SELECT WHERE no = 59

변경 전 데이터를 언두 로그로 복사

DDUZY로 조회(언두 로그의 이전 데이터를 조회)

COMMIT

no: 59 name: OGU

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

트랜잭션 1
DATABASE
트랜잭션 2

BEGIN

no: 59 name: OGU

SELECT WHERE no = 59

OGU로 조회

BEGIN

UPDATE SET name = 'DDUZY' WHERE no = 59

COMMIT

no: 59 name: DDUZY

SELECT WHERE no = 59(같은 쿼리 실행)

DDUZY로 조회

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

REPEATABLE READ

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

  • 트랜잭션이 시작되는 시점에 스냅샷을 생성하여, 이후 트랜잭션 내의 모든 SELECT 쿼리는 해당 스냅샷을 기준으로 데이터를 조회(=MVCC)

  • 다른 트랜잭션이 데이터를 변경하고 커밋하는 경우, 현재 트랜잭션은 자신의 트랜잭션 번호보다 낮은 트랜잭션 번호를 가진 언두 로그의 데이터를 조회

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

모든 InnoDB 트랜잭션에는 고유한 트랜잭션 번호(순차적 증가 값)가 부여되며, 언두 영역에 백업된 레코드에는 트랜잭션 번호가 저장된다.

트랜잭션 12
DATABASE
트랜잭션 10

TRX-ID: 6 no: 59 name: DDUZY

BEGIN(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로 조회

트랜잭션 번호를 이용해 트랜잭션 12에서 데이터가 변경되었지만, 트랜잭션 10은 자신의 번호보다 작은 트랜잭션의 번호를 조회하여 언두 로그에 있는 데이터를 조회하게 된다.

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

위와 같이 잠금 없이 조회하는 경우엔 MVCC를 통해 데이터 무결성을 보장하지만, 베타적 잠금을 걸게 되면 PHANTOM READ가 발생할 수 있다.

일반적인 DBMS 기준

일반적인 DBMS에서 베타적 잠금을 걸어 조회하게 되면 PHANTOM READ가 발생하여 동일한 쿼리를 실행했을 때 결과가 달라질 수 있다.

트랜잭션 12
DATABASE
트랜잭션 10

TRX-ID: 6 no: 59 name: DDUZY

BEGIN(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 조회

id = 59인 레코드에 대해서만 잠금이 걸리고, 트랜잭션 12의 요청은 잠금 없이 즉시 실행이 되고, 이후 조회에서 없던 id = 60인 레코드가 조회되어 PHANTOM READ가 발생하게 된다.

InnoDB 스토리지 MySQL 기준

하지만 MySQL에서는 넥스트 키 락이 존재하기 때문에 트랜잭션 12의 요청이 잠금을 획득하기 위해 대기하게 되고, 결국에는 트랜잭션 10은 DDUZY만 조회하게 된다.

트랜잭션 12
DATABASE
트랜잭션 10

TRX-ID: 6 no: 59 name: DDUZY

BEGIN(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

갭락 존재로 일반적으로 MySQL에서는 Phantom Read가 발생하지 않지만, 발생 할 수 있는 시나리오는 다음과 같다.

  1. 첫 번째 조회는 잠금 없이 조회

  2. 두 번째 조회에서 베타적 잠금을 획득하여 조회

하지만 이렇게 조회하는 경우는 일반적으로 없기 때문에 Phantom Read가 거의 발생하지 않는다고 볼 수 있다.

SERIALIZABLE

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

  • 읽기 작업도 공유 잠금(읽기 잠금)을 획득을 설정

  • 잠금이 걸린 경우 다른 트랜잭션에서 해당 레코드를 변경하거나 삭제할 수 없음

하지만 InnoDB 스토리지 엔진에서는 READ COMMITTED 격리 수준에서도 PHANTOM READ 문제를 해결할 수 있어 해당 격리 수준을 사용하지 않는다.

참고자료

Last updated

Was this helpful?