같은 트랜잭션인데 두 번 조회한 값이 달랐던 이유 — 격리 수준
문제 발생
하나의 트랜잭션 안에서 같은 SELECT 쿼리를 두 번 실행했는데, 그 사이에 값이 달라져 있었습니다 — 트랜잭션 중간에 다른 세션이 그 데이터를 커밋한 것이 원인이었습니다.
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 100
-- (이 사이에 다른 트랜잭션이 balance를 150으로 바꾸고 커밋함)
SELECT balance FROM accounts WHERE id = 1; -- 150 -- 같은 트랜잭션인데 값이 다름
COMMIT;원인 분석
이 현상을 Non-repeatable read라고 부르며, 데이터베이스의 격리 수준(isolation level) 설정에 따라 발생 여부가 달라집니다.
- Read Committed(PostgreSQL, MySQL InnoDB 등의 흔한 기본값 중 하나 — 실제 기본값은 DB와 버전마다 다르므로 확인이 필요합니다): 각 쿼리는 그 쿼리를 실행하는 시점에 커밋된 최신 데이터를 봅니다. 그래서 같은 트랜잭션 안에서도 두 조회 사이에 다른 트랜잭션이 커밋했다면 값이 달라 보일 수 있습니다 — 위 예시가 정확히 이 케이스입니다.
- Repeatable Read: 트랜잭션이 시작된 시점의 스냅샷을 트랜잭션이 끝날 때까지 계속 봅니다. 트랜잭션 도중 다른 세션이 값을 바꾸고 커밋해도, 같은 트랜잭션 안에서는 항상 같은 값을 보게 됩니다.
SQL 표준은 이 외에도 Read Uncommitted(커밋 안 된 값까지 보임, dirty read 허용), Serializable(가장 엄격, 트랜잭션들이 순차적으로 실행된 것과 같은 결과를 보장)까지 총 네 단계를 정의합니다. 단계가 엄격할수록 데이터 일관성은 강해지지만 동시성(성능)은 낮아지는 트레이드오프가 있습니다.
해결 방안
- 애플리케이션이 트랜잭션 안에서 같은 데이터를 여러 번 읽고 그 값이 일관되어야 한다면(예: 계산 도중 값이 바뀌면 안 되는 로직), Repeatable Read 이상의 격리 수준을 명시적으로 요구합니다.
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;- 반대로 최신 값을 최대한 빨리 반영해야 하는 일반적인 조회 위주 트랜잭션은 기본값(대부분 Read Committed)으로 충분하고, 굳이 더 엄격한 수준으로 올리면 잠금 경합이나 재시도(serialization failure)가 늘어나 성능이 떨어질 수 있습니다.
- 각 DB(PostgreSQL, MySQL, Oracle 등)의 실제 기본 격리 수준과 구현 방식은 서로 다릅니다 — "Read Committed면 항상 이렇게 동작한다"고 일반화하기 전에, 실제 사용 중인 DB의 공식 문서에서 그 DB의 정확한 구현을 확인해야 합니다.
- 동시 수정 충돌이 실제로 비즈니스에 영향을 준다면(재고 차감, 잔액 이체 등), 격리 수준만으로 해결하려 하지 말고 낙관적 잠금(버전 컬럼)이나 명시적
SELECT ... FOR UPDATE같은 락 전략을 함께 검토합니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.