트랜잭션 격리 수준

k_bell·2025년 7월 8일

데이터베이스

목록 보기
12/12
post-thumbnail

데이터베이스를 다루다 보면 트랜잭션의 격리 수준(Isolation Level)이라는 개념을 만나게 됩니다. 오늘은 각 격리 수준에서 발생할 수 있는 문제들과 MySQL의 독특한 특성에 대해 알아보겠습니다.

READ UNCOMMITTED: 가장 낮은 격리 수준

READ UNCOMMITTED는 가장 낮은 격리 수준으로, 커밋되지 않은 데이터까지 읽을 수 있습니다. 이로 인해 Dirty Read라는 문제가 발생합니다.

Dirty Read ?

다른 트랜잭션에서 수정했지만 아직 커밋하지 않은 데이터를 읽는 현상입니다. 문제는 해당 트랜잭션이 롤백되면 읽었던 데이터가 실제로는 존재하지 않는 값이 된다는 점입니다.

예시 상황:

-- T1 트랜잭션
set transaction isolation level read uncommitted;
start transaction;

select * from users where id = 1; -- 최초 조회: age = 30
select * from users where id = 1; -- Dirty Read: age = 21 (아직 커밋되지 않은 값!)
select * from users where id = 1; -- 롤백 후: age = 30 (원래 값으로 복구)

commit;
-- T2 트랜잭션 (동시에 실행)
start transaction;
update users set age = 21 where id = 1;
rollback; -- 변경사항을 취소!

T1에서 두 번째 조회 시 읽은 21이라는 값은 결국 존재하지 않는 데이터가 되어버립니다. 이것이 바로 데이터 일관성이 깨지는 순간입니다.

READ COMMITTED: 커밋된 데이터만 읽기

READ COMMITTED는 커밋된 데이터만 읽을 수 있는 격리 수준입니다. Dirty Read는 해결되지만, 새로운 문제들이 등장합니다.

Non-Repeatable Read

같은 트랜잭션 내에서 같은 데이터를 두 번 조회했을 때 결과가 달라지는 현상입니다.

-- T1 트랜잭션
set transaction isolation level read committed;
start transaction;

select * from account where id = 1; -- 최초 조회: balance = 1000
select * from account where id = 1; -- 두 번째 조회: balance = 900 (다른 값!)

commit;
-- T2 트랜잭션 (동시에 실행)
start transaction;
update account set balance = 900 where id = 1;
commit; -- 이 시점에서 변경사항이 확정됨

Phantom Read

같은 조건으로 조회했을 때 레코드의 개수가 달라지는 현상입니다.

-- T1 트랜잭션
set transaction isolation level read committed;
start transaction;

-- 최초 조회: 1개의 레코드
select * from account where balance between 1000 and 2000; 

-- 두 번째 조회: 2개의 레코드 (새로운 레코드가 나타남!)
select * from account where balance between 1000 and 2000; 

commit;
-- T2 트랜잭션 (동시에 실행)
start transaction;
insert into account values (2, 2000); -- 새로운 레코드 삽입
commit;

마치 유령(Phantom)이 나타난 것처럼 레코드가 추가로 조회되어서 Phantom Read라고 부릅니다.

REPEATABLE READ: 일관된 읽기 보장

REPEATABLE READ는 동일 트랜잭션 내에서는 항상 동일한 결과를 보장하는 격리 수준입니다.

-- T1 트랜잭션
set transaction isolation level repeatable read;
start transaction;

-- 최초 조회: 1개의 레코드
select * from account where balance between 1000 and 2000; 

-- 두 번째 조회: 여전히 1개의 레코드 (일관된 결과!)
select * from account where balance between 1000 and 2000; 

commit;

다른 트랜잭션에서 새로운 레코드를 삽입해도 T1에서는 여전히 동일한 결과를 조회합니다.

MySQL의 특별한 점: Phantom Read가 발생하지 않는 이유

일반적으로 REPEATABLE READ 수준에서도 Phantom Read가 발생할 수 있다고 알려져 있습니다. 하지만 MySQL에서는 REPEATABLE READ가 기본 격리 수준임에도 불구하고 Phantom Read가 발생하지 않습니다.

락(Lock) 메커니즘의 비밀

MySQL은 다음과 같은 락 메커니즘을 사용합니다:

  1. Record Lock: 특정 레코드에 대한 잠금
  2. Gap Lock: 레코드 사이의 간격에 대한 잠금
  3. Next Key Lock: Record Lock과 Gap Lock을 동시에 적용

실제 예시:

-- Session 1
SET transaction_isolation='REPEATABLE-READ';
BEGIN;
SELECT * FROM tb_gaplock WHERE id BETWEEN 1 AND 3 FOR UPDATE;
-- Session 2 (동시에 실행)
SET transaction_isolation='REPEATABLE-READ';
BEGIN;
INSERT INTO tb_gaplock VALUES (2, 'Matt2'); -- 대기 상태가 됨!

Session 1에서 FOR UPDATE로 조회할 때, MySQL은 해당 범위의 레코드뿐만 아니라 그 사이의 간격(Gap)까지 잠금을 겁니다. 따라서 Session 2에서 새로운 레코드를 삽입하려고 해도 잠금에 걸려서 대기하게 되죠.

Gap Lock 사용 시 주의사항

Gap Lock을 사용할 때는 몇 가지 주의해야 할 점이 있습니다:

  • 레코드가 없는 경우: 조회 결과가 없어도 Gap Lock이 적용될 수 있어 예상치 못한 대기 상황이 발생할 수 있습니다.
  • 성능 영향: 락의 범위가 넓어질수록 동시성이 떨어질 수 있습니다.

마무리

트랜잭션 격리 수준은 데이터의 일관성과 동시성 사이의 균형을 맞추는 중요한 개념입니다. 각 격리 수준의 특성을 이해하고 상황에 맞게 선택하는 것이 중요합니다.

  • READ UNCOMMITTED: 가장 빠르지만 데이터 일관성 문제 발생
  • READ COMMITTED: 커밋된 데이터만 읽지만 반복 조회 시 결과가 달라질 수 있음
  • REPEATABLE READ: 일관된 읽기 보장, MySQL에서는 Phantom Read까지 방지
  • SERIALIZABLE: 가장 엄격한 격리 수준 (오늘 다루지 않았지만 존재합니다)

MySQL의 기본 격리 수준인 REPEATABLE READ가 다른 데이터베이스보다 더 강력한 일관성을 제공한다는 점도 기억해두면 좋을 것 같습니다.

0개의 댓글