지난 cloud computing 수업에서 phantom read를 다룬 적이 있다.
오늘은 phantom read에 대해서 알아보고, 이를 어떻게 방지할 것인지 알아보고자 한다.
Phantom read란?!
Phantom Read는 데이터베이스의 트랜잭션 격리 수준에 따라 발생할 수 있는 동시성 제어 이슈 중 하나다.
두번의 동일한 query 사이에서 다른 트랜젝션에 의해 행이 INSERT되거나 삭제되어 기존에 없던 데이터가 생기거나 사라지는 현상이다.
코드로 살펴보면 다음과 같다.
INSERT INTO employees (salary) VALUES (1000), (1500), (2000);
-- Transaction A 시작
START TRANSACTION;
SELECT COUNT(*) AS cnt_before
FROM employees
WHERE salary > 1000;
-- 결과가 2가 나와야 함
-- Transaction B 시작
START TRANSACTION;
INSERT INTO employees (salary) VALUES (1800);
COMMIT;
-- Transaction A에서 재조회(A는 아직 commit하지 않음)
SELECT COUNT(*) AS cnt_after
FROM employees
WHERE salary > 1000;
-- 기존 결과는 2였지만 지금은 3이 나오게 된다.
A Transaction 도중에 새로운 행이 삽입되어 처음에는 2건이였던 조회 결과가 3건으로 늘어나 Transaction중에 변한 것을 확인할 수 있다.
이와 비슷한 동시성 제어 이슈로는 dirty read, non-repeatable read가 있는데 이는 다음에 알아보겠다.
먼저 이 현상을 이해하려 하면 트랜잭션 격리 수준에 대하여 알아야 한다.
트랜잭션 격리 수준
추후 자세히 다루겠지만, 트랜잭션 격리 수준은 4가지로 나뉜다.
- READ UNCOMMITTED→ 모든 동시성 이슈가 발생할 수 있다.
→ 동시성 성능은 가장 좋지만, 데이터 무결성 측면에서 매우 위험하다.
아직 커밋되지 않은 데이터를 다른 트랜잭션에서 읽을 수 있다. - READ COMMITTED
→ Dirty Read는 방지하지만, Non-Repeatable Read와 Phantom Read는 발생할 수 있다.
커밋이 완료된 데이터만 읽을 수 있다. - REPEATABLE READ
같은 트랜잭션 내에서는 같은 행을 여러번 조회해도 동일한 값을 보장한다.(ex, MySQL InnoDB에서는 갭락을 이용하여 방지한다)
→ 표준 정의 상 Phantom Read가 발생할 수 있지만, DB엔진의 구현에 따라 방지될 수 있다.
기본 설정으로 많이 사용되는 격리 수준이다. - SERIALIZABLE
→ 사실상 모든 동시성 제어 이슈로부터 자유롭다.
다만, 실행에 필요한 리소스 제한으로 성능 저하로 이어지기 쉽다
모든 트랜잭션이 Serialize하게 실행되는 것과 같은 결과를 보장한다.
이렇듯, GapLock과 NextKeyLock을 이용하여 트랜잭션에서의 데이터 무결성을 보장하고자 하는 것을 볼 수 있다.
Gap Lock과 Index Lock은?
Gap Lock
→ 인덱스 레코드 사이 간격에 걸리는 락이다.
범위 내의 특정 레코드가 존재하지 않을 때 적용된다.
트랜잭션이 특정 범위(인덱스 레코드 사이의 간격, gap)의 데이터 삽입을 막아서 phantom Read를 방지한다.
-- Transaction A 시작
START TRANSACTION;
-- salary가 2000 초과 ~ 4000 미만인 레코드를 범위 잠금 (FOR UPDATE)
SELECT *
FROM employees
WHERE salary > 2000 AND salary < 4000
FOR UPDATE;
-- Transaction B 시작
START TRANSACTION;
-- 갭 범위에 해당하는 2500은 락으로 인해 A가 commit이나 rollback할 때 까지 대기
INSERT INTO employees (salary) VALUES (2500);
Next-Key Lock
Record Lock과 Gap Lock이 결합된 형테이다.
해당 레코드 자체와 주변의 갭을 동시에 잠그는 락이다.
→ 이 락을 통해서 새로운 레코드를 삽입을 방지해 phantom read를 방지한다.
-- Transaction A 시작
START TRANSACTION;
-- salary가 3000인 레코드를 잠금 (FOR UPDATE)
SELECT *
FROM employees
WHERE salary = 3000
FOR UPDATE;
-- Transaction B 시작
START TRANSACTION;
-- salery=3000 레코드를 수정하려 하지만 락이 걸려 A가 commit이나 rollback할 때 까지 대기
UPDATE employees
SET salary = 3500
WHERE salary = 3000;
-- 마찬가지로 인접 인덱스에 삽입을 시도하지만 락이 걸려 A가 commit이나 rollback할 때 까지 대기
INSERT INTO employees (salary) VALUES (3200);
주의점
- 과도한 갭 락으로 인한 성능 저하 가능성
→ 범위가 넓은 쿼리가 발생하면 중간 범위에 insert하려는 트랜잭션들이 모두 대기 상태에 빠질 수 있다. - 데드락 가능성
→ Lock으로 인한 서로 다른 범위를 잠근 트랜잭션이 deadlock에 빠질 수 있어서 이를 판단하고 재시도하는 로직이 필요할 수 있다.
본 게시글은 ‘매일메일’ 서비스의 뉴스레터를 읽고 추가 및 재구성한 글입니다.
매일메일 - 기술 면접 질문 구독 서비스
기술 면접 질문을 매일매일 메일로 보내드릴게요!
www.maeil-mail.kr
'Database' 카테고리의 다른 글
RDB와 커서 기반 페이징을 사용하는 이유 (0) | 2025.04.15 |
---|---|
QueryDSL의 HQL injection 방지하기 (1) | 2024.12.20 |