데이터베이스를 다루다 보면 트랜잭션의 격리 수준(Isolation Level)이라는 개념을 만나게 됩니다. 오늘은 각 격리 수준에서 발생할 수 있는 문제들과 MySQL의 독특한 특성에 대해 알아보겠습니다.
READ UNCOMMITTED는 가장 낮은 격리 수준으로, 커밋되지 않은 데이터까지 읽을 수 있습니다. 이로 인해 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는 커밋된 데이터만 읽을 수 있는 격리 수준입니다. Dirty 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; -- 이 시점에서 변경사항이 확정됨
같은 조건으로 조회했을 때 레코드의 개수가 달라지는 현상입니다.
-- 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는 동일 트랜잭션 내에서는 항상 동일한 결과를 보장하는 격리 수준입니다.
-- 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에서는 여전히 동일한 결과를 조회합니다.
일반적으로 REPEATABLE READ 수준에서도 Phantom Read가 발생할 수 있다고 알려져 있습니다. 하지만 MySQL에서는 REPEATABLE READ가 기본 격리 수준임에도 불구하고 Phantom Read가 발생하지 않습니다.
MySQL은 다음과 같은 락 메커니즘을 사용합니다:
실제 예시:
-- 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을 사용할 때는 몇 가지 주의해야 할 점이 있습니다:
트랜잭션 격리 수준은 데이터의 일관성과 동시성 사이의 균형을 맞추는 중요한 개념입니다. 각 격리 수준의 특성을 이해하고 상황에 맞게 선택하는 것이 중요합니다.
MySQL의 기본 격리 수준인 REPEATABLE READ가 다른 데이터베이스보다 더 강력한 일관성을 제공한다는 점도 기억해두면 좋을 것 같습니다.