본문으로 건너뛰기
개발 머꼬
개발 노트SQL
hohyeon.dev23

같은 트랜잭션인데 두 번 조회한 값이 달랐던 이유 — 격리 수준

  • #Database
  • #Engineering Note
  • #Transaction

문제 발생

하나의 트랜잭션 안에서 같은 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(가장 엄격, 트랜잭션들이 순차적으로 실행된 것과 같은 결과를 보장)까지 총 네 단계를 정의합니다. 단계가 엄격할수록 데이터 일관성은 강해지지만 동시성(성능)은 낮아지는 트레이드오프가 있습니다.

해결 방안

  1. 애플리케이션이 트랜잭션 안에서 같은 데이터를 여러 번 읽고 그 값이 일관되어야 한다면(예: 계산 도중 값이 바뀌면 안 되는 로직), Repeatable Read 이상의 격리 수준을 명시적으로 요구합니다.
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
  1. 반대로 최신 값을 최대한 빨리 반영해야 하는 일반적인 조회 위주 트랜잭션은 기본값(대부분 Read Committed)으로 충분하고, 굳이 더 엄격한 수준으로 올리면 잠금 경합이나 재시도(serialization failure)가 늘어나 성능이 떨어질 수 있습니다.
  2. 각 DB(PostgreSQL, MySQL, Oracle 등)의 실제 기본 격리 수준과 구현 방식은 서로 다릅니다 — "Read Committed면 항상 이렇게 동작한다"고 일반화하기 전에, 실제 사용 중인 DB의 공식 문서에서 그 DB의 정확한 구현을 확인해야 합니다.
  3. 동시 수정 충돌이 실제로 비즈니스에 영향을 준다면(재고 차감, 잔액 이체 등), 격리 수준만으로 해결하려 하지 말고 낙관적 잠금(버전 컬럼)이나 명시적 SELECT ... FOR UPDATE 같은 락 전략을 함께 검토합니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.