Dirty Read와 Phantom Read: 트랜잭션 격리성에서 생기는 데이터 불일치

Dirty Read와 Phantom Read의 발생 조건, 격리 수준별 차이, DBMS 구현 특성과 운영 시 점검 방법을 정리합니다.

2026-08-14 · 최초 발행 2025-08-10

커밋 전 데이터를 읽으면 생기는 일

Dirty Read는 한 트랜잭션이 아직 커밋되지 않은 다른 트랜잭션의 변경 내용을 읽는 현상이다. 변경한 쪽이 나중에 롤백하면, 읽은 쪽은 실제로 존재하지 않는 데이터를 기준으로 후속 처리를 하게 된다.

은행 계좌 이체를 예로 들 수 있다. 트랜잭션 A가 계좌 잔액을 100만원에서 50만원으로 변경했지만 아직 커밋하지 않은 상태에서, 트랜잭션 B가 잔액을 읽는다. 이후 트랜잭션 A가 오류로 롤백되면 실제 잔액은 100만원으로 돌아가지만, 트랜잭션 B는 50만원이라는 값을 기준으로 판단했을 수 있다.

이런 읽기는 잘못된 데이터에 기반한 의사결정, 비즈니스 로직 오류, 데이터 무결성 훼손으로 이어질 수 있다.

트랜잭션 BDB트랜잭션 A트랜잭션 BDB트랜잭션 ADirty Read 발생!트랜잭션 B는 유효하지 않은 데이터를 가짐데이터 수정 (커밋 전)수정된 데이터 읽기롤백

동일한 조건으로 다시 조회했는데 결과가 바뀌는 경우

Phantom Read는 하나의 트랜잭션이 같은 쿼리를 두 번 실행했을 때, 첫 조회 뒤에 다른 트랜잭션이 추가하거나 삭제한 레코드가 두 번째 결과에 포함되는 현상이다. 조건에 맞는 레코드 집합 자체가 달라지므로, 같은 트랜잭션 안에서도 조회 결과의 일관성이 깨진다.

재고 관리 시스템에서 트랜잭션 A가 가격이 10,000원 이하인 상품을 조회해 10개를 확인했다고 하자. 트랜잭션 B가 가격이 9,000원인 새 상품을 추가하고 커밋한 뒤, 트랜잭션 A가 같은 조건으로 다시 조회하면 11개 상품이 표시된다.

통계 집계가 부정확해지거나 페이지네이션에서 누락·중복이 발생할 수 있다.

트랜잭션 BDB트랜잭션 A트랜잭션 BDB트랜잭션 APhantom Read 발생!SELECT WHERE 조건 (결과: 10개 레코드)INSERT 새 레코드 (조건에 일치)COMMIT동일 SELECT 쿼리 재실행 (결과: 11개 레코드)

격리 수준마다 허용하는 읽기의 범위

SQL 표준은 트랜잭션 격리 수준을 네 가지로 정의한다. 낮은 수준일수록 동시 처리 성능에는 유리할 수 있지만, 읽기 일관성에 대한 보장은 줄어든다.

READ UNCOMMITTED는 가장 낮은 수준으로 Dirty Read, Non-repeatable Read, Phantom Read가 모두 발생할 수 있다. 성능은 가장 좋으나 데이터 일관성을 보장할 수 없다.

READ COMMITTED에서는 각 쿼리가 커밋된 데이터만 읽으므로 Dirty Read를 방지한다. 다만 Non-repeatable Read와 Phantom Read는 발생할 수 있으며, Oracle, SQL Server, PostgreSQL 등의 기본 격리 수준이다.

REPEATABLE READ는 트랜잭션 안에서 같은 쿼리 결과가 항상 동일하도록 보장한다. Dirty Read와 Non-repeatable Read는 막지만 Phantom Read는 발생할 수 있다. MySQL InnoDB의 기본 격리 수준이다.

SERIALIZABLE은 Dirty Read, Non-repeatable Read, Phantom Read를 모두 방지하고 완전한 트랜잭션 격리를 제공한다. 대신 성능 저하가 심각할 수 있다.

방지 없음Dirty Read 방지Dirty Read, Non-repeatableRead 방지모든 문제 방지트랜잭션 격리 수준READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE모든 문제 발생 가능Non-repeatable Read,Phantom Read 발생 가능Phantom Read 발생 가능완벽한 격리, 성능 저하

DBMS 구현에서 확인할 차이

Oracle의 기본 격리 수준은 READ COMMITTED다. SERIALIZABLE은 스냅샷 격리(Snapshot Isolation) 방식으로 구현하며, SELECT FOR UPDATE로 행 잠금을 제공한다.

MySQL InnoDB는 REPEATABLE READ를 기본 격리 수준으로 사용하고 MVCC(Multi-Version Concurrency Control)를 사용한다. REPEATABLE READ에서도 갭 락(Gap Lock)을 통해 일부 Phantom Read를 방지한다.

SQL Server는 READ COMMITTED가 기본이며, SNAPSHOT 격리 수준도 제공한다. 낙관적 동시성 제어(Optimistic Concurrency Control)를 지원한다.

PostgreSQL 역시 기본은 READ COMMITTED다. SERIALIZABLE 구현에는 SSI(Serializable Snapshot Isolation)를 사용하며, 트랜잭션 충돌 시 명시적 에러를 발생시킨다.

업무 정확도에 맞춘 격리 수준 선택

읽기 전용 분석 쿼리에는 READ UNCOMMITTED를 주의하여 사용할 수 있다. 일반적인 웹 애플리케이션에는 READ COMMITTED가 해당하며, 금융 거래나 중요한 비즈니스 로직에는 REPEATABLE READ 또는 SERIALIZABLE을 고려할 수 있다.

격리 수준을 일괄적으로 높이는 대신 트랜잭션 범위를 줄여 잠금 시간을 낮추고, 중요한 작업에만 높은 수준을 적용하는 방식이 필요하다. SELECT FOR UPDATE 같은 명시적 잠금과 애플리케이션 레벨의 추가 검증 로직도 함께 검토할 수 있다.

Spring에서는 다음과 같이 트랜잭션에 격리 수준을 지정할 수 있다.

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transferMoney(long fromId, long toId, BigDecimal amount) {
    Account fromAccount = accountRepository.findById(fromId);
    Account toAccount = accountRepository.findById(toId);

    fromAccount.withdraw(amount);
    toAccount.deposit(amount);

    accountRepository.save(fromAccount);
    accountRepository.save(toAccount);
}

운영 중 불일치 징후를 찾는 방법

Dirty Read는 애플리케이션 로그에서 비정상적인 데이터 값 패턴을 확인하고, 트랜잭션 롤백 뒤 데이터 불일치가 발생하는지 검사하는 방식으로 탐지할 수 있다. 테스트 환경에서는 의도적인 롤백 시나리오를 실행해 확인한다.

Phantom Read는 같은 트랜잭션에서 실행한 동일 쿼리의 레코드 수를 비교하거나, COUNT, SUM 같은 집계 함수 결과의 불일치를 모니터링해 점검할 수 있다. 페이지네이션에서는 누락 또는 중복 레코드도 확인 대상이다.

DBMS별로는 Oracle의 Automatic Workload Repository(AWR), MySQL의 Performance Schema와 SHOW ENGINE INNODB STATUS, SQL Server의 Dynamic Management Views(DMVs), PostgreSQL의 pg_stat_activitypg_locks를 활용할 수 있다.

격리 수준은 단순한 설정값이 아니라 비즈니스 요구사항과 시스템 아키텍처를 함께 반영하는 선택이다. 금융이나 재고 관리처럼 데이터 정확성이 중요한 시스템에서는 특히 DBMS별 동작 특성과 성능 요건을 함께 검토해야 한다.

트랜잭션데이터베이스격리성동시성 제어MVCC