데이터베이스를 사용하는 애플리케이션에서 여러 사용자가 동시에 데이터를 읽고 쓰는 작업은 흔하게 발생합니다. 이때, 동시성 문제로 인해 데이터의 일관성이 깨지는 것을 방지하는 것이 매우 중요합니다. 이를 위해 데이터베이스는 트랜잭션(Transaction)이라는 개념을 사용하며, 트랜잭션의 핵심 속성 중 하나가 바로 격리(Isolation)입니다. 오늘은 이 격리 수준에 대해 심도 있게 알아보겠습니다.
트랜잭션은 데이터베이스의 논리적인 작업 단위로, ACID(Atomicity, Consistency, Isolation, Durability) 속성을 만족해야 합니다. 이 중 격리(Isolation)는 동시에 실행되는 트랜잭션들이 서로에게 영향을 주지 않고 독립적으로 실행되는 것을 보장하는 속성입니다. 마치 여러 명이 같은 문서를 수정할 때, 각자가 자신의 수정 내용을 다른 사람에게 방해받지 않고 적용하는 것과 같습니다. 하지만 완벽한 격리는 시스템의 성능 저하로 이어질 수 있기에, SQL 표준에서는 다양한 격리 수준을 정의하여 개발자가 필요에 따라 선택할 수 있도록 합니다.
트랜잭션 격리 수준은 크게 네 가지로 나뉘며, 낮은 수준일수록 동시성은 높지만 데이터 일관성 문제는 발생할 가능성이 커지고, 높은 수준일수록 동시성은 낮아지지만 데이터 일관성은 완벽하게 보장됩니다. 각 격리 수준을 이해하기 전에, 먼저 격리 수준에 따라 발생할 수 있는 주요 동시성 문제들을 살펴보겠습니다.
더티 리드(Dirty Read): 아직 커밋되지 않은(롤백될 수도 있는) 다른 트랜잭션의 데이터를 읽는 현상입니다. 만약 읽은 데이터가 롤백되면, 현재 트랜잭션은 유효하지 않은 데이터를 기반으로 작업을 수행하게 됩니다.
반복 불가능한 읽기(Non-Repeatable Read): 한 트랜잭션 내에서 같은 쿼리를 두 번 수행했을 때, 다른 트랜잭션이 그 사이에 데이터를 수정하거나 삭제하여 첫 번째와 두 번째 쿼리의 결과가 다르게 나타나는 현상입니다.
환영 읽기(Phantom Read): 한 트랜잭션 내에서 특정 조건으로 레코드를 조회했을 때, 다른 트랜잭션이 그 사이에 새로운 레코드를 삽입하여 첫 번째와 두 번째 쿼리의 결과 집합(레코드 수)이 다르게 나타나는 현상입니다.
이제 각 격리 수준이 이러한 문제들을 어떻게 방지하는지 알아보겠습니다.
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 트랜잭션 시작
BEGIN TRANSACTION;
SELECT * FROM accounts WHERE id = 1;
-- 다른 트랜잭션이 id=1의 잔액을 변경하고 커밋하지 않음
SELECT * FROM accounts WHERE id = 1; -- 변경된(커밋되지 않은) 데이터를 읽을 수 있음
COMMIT;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 트랜잭션 시작
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- 100
-- (다른 트랜잭션) UPDATE accounts SET balance = 150 WHERE id = 1; COMMIT;
SELECT balance FROM accounts WHERE id = 1; -- 150 (반복 불가능한 읽기 발생)
COMMIT;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 트랜잭션 시작
BEGIN TRANSACTION;
SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- 5개
-- (다른 트랜잭션) INSERT INTO orders (status) VALUES ('pending'); COMMIT;
SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- 여전히 5개 (환영 읽기 발생 가능성 있음, DB 구현체에 따라 다름)
COMMIT;
Note: MySQL InnoDB의 REPEATABLE READ는 기본적으로 넥스트 키 락(Next-Key Lock)을 사용하여 팬텀 리드까지 방지하는 구현이 많습니다. 하지만 SQL 표준에서는 팬텀 리드가 발생할 수 있다고 정의합니다.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 트랜잭션 시작
BEGIN TRANSACTION;
SELECT SUM(amount) FROM transactions WHERE account_id = 1;
-- (다른 트랜잭션) INSERT INTO transactions ...; 또는 UPDATE transactions ...; 시도 시 대기 (Lock 발생)
SELECT SUM(amount) FROM transactions WHERE account_id = 1; -- 첫 번째 쿼리와 동일한 결과 보장
COMMIT;
각 격리 수준은 장단점을 가지고 있으므로, 애플리케이션의 특성과 요구사항에 맞춰 신중하게 선택해야 합니다.
READ UNCOMMITTED (권장하지 않음)READ COMMITTED (일반적으로 좋은 균형점)REPEATABLE READSERIALIZABLE대부분의 경우 READ COMMITTED 또는 REPEATABLE READ가 좋은 출발점이 됩니다. 하지만 사용 중인 데이터베이스 시스템(PostgreSQL, MySQL, Oracle 등)마다 격리 수준의 구현 방식과 기본값이 다를 수 있으므로, 해당 데이터베이스의 문서를 참고하여 정확히 이해하는 것이 중요합니다. 효율적인 격리 수준 선택은 안정적인 서비스 운영과 직결됩니다.
Text by Chaelin & Gemini. Photographs by Chaelin, Unsplash.