pile·
DB / 데이터·네이버페이네이버페이·

실무에서 만나는 DB isolation level

네이버페이 개발자가 Oracle에서 MySQL로 옮긴 결제 시스템에서 겪은 동시성 버그를 통해 DB 격리 수준을 실무 관점에서 풀어낸다. 잔액 검증 로직이 자꾸 실패했는데 원인은 코드가 아니라 MySQL의 기본 격리 수준 REPEATABLE READ였다. 같은 트랜잭션에서 한 번 읽은 값이 고정되는 특성이 lock 이전의 select와 만나 버그를 냈다.

핵심 포인트
  • 결제는 '업데이트 전 잔액 == 결제 이전 잔액'일 때만 갱신하는 낙관적 방식이라 동시성 제어가 필수였다.
  • 동시 요청을 막으려 트랜잭션 앞에서 잔액 컬럼을 UPDATE해 DB lock을 잡았다.
  • lock을 잡기 전에 '잔액 정보 없으면 insert'하는 select 로직을 추가한 순간, 두 번째 이후 요청의 갱신이 실패했다.
  • 원인은 MySQL 기본 격리 수준 REPEATABLE READ다. 트랜잭션 내에서 처음 읽은 잔액이 커밋 변화와 무관하게 고정된다.
  • Oracle은 기본이 READ COMMITTED라 같은 코드가 문제없었다. 격리 수준은 상황·정책에 맞춰 골라야 한다.
상세 정리
  • 문제 코드: processPayment가 결제 후 'balance = originalBalance'일 때만 잔액을 UPDATE한다. 순차 요청은 정상이었다.
  • lock 도입: 트랜잭션 시작부에서 lockUserById로 잔액 컬럼을 UPDATE해 lock을 잡아, 두 번째 요청은 첫 요청이 끝날 때까지 대기하게 했다.
  • 회귀: lock 직전에 readUserMethod(잔액 없으면 기본값 insert)를 추가하자 두 번째 이후 요청이 잔액 갱신에 실패했다.
  • 증상: A와 B가 동시에 들어오면 A가 먼저 lock을 잡고 B는 대기한다. 기대와 달리 B는 A가 커밋한 잔액이 아니라 커밋 이전 값을 계속 읽었다.
  • 원인 분석: readUserMethod가 lock 이전에 잔액을 select하고, REPEATABLE READ가 그 값을 트랜잭션 끝까지 고정한다. A·B 모두 10,000원을 읽은 뒤 A가 5,000원을 커밋해도 B는 10,000원으로 결제를 이어갔다.
  • 해결책 1: 잔액 읽기를 lockUserById 뒤로 옮겨 lock 안에서 커밋된 데이터만 읽게 한다.
  • 해결책 2: 해당 트랜잭션만 @Transactional(isolation = Isolation.READ_COMMITTED)로 낮춘다.
  • 해결책 3: DB 기본 격리 수준을 READ COMMITTED로 바꾼다(DBA 협의).
  • Oracle과의 차이: 구 시스템이 멀쩡했던 건 Oracle 기본이 READ COMMITTED라 중간에 커밋된 값을 다시 읽어왔기 때문이다. MySQL은 REPEATABLE READ라 문제가 드러났다.
  • 트레이드오프: 결제 도중 아이템 가격이 1,000→1,500원으로 바뀌면 READ COMMITTED는 그 변경을 읽어 잔액부족 결제 실패를 낼 수 있다. REPEATABLE READ가 높은 격리라 해서 모든 상황의 정합성을 보장하진 않는다.
  • 교훈: RDBMS마다 기본·지원 격리 수준이 다르고(Oracle은 REPEATABLE READ 미지원) 프레임워크 설정값도 함께 살펴야 한다.
왜 읽나격리 수준을 암기만 했던 개발자가 실제 결제 동시성 버그로 REPEATABLE READ와 READ COMMITTED의 차이를 체감하고 싶을 때 유용하다.
네이버페이
네이버페이 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. DB / 데이터·spotify-engSpotify Engineering·

    데이터 레이크 온라인 포인트 쿼리 인덱싱 — RAP

    Spotify 엔지니어링이 페타바이트 규모 데이터 레이크(GCS)에 저장된 Parquet 파일에서 한 자릿수 밀리초 레이턴시로 포인트 쿼리를 수행하는 RAP(Random Access Parquet) 방식을 공개했다. Trino·BigQuery 같은 분산 SQL 엔진은 단일 행 조회도 스케줄링·플래닝 비용으로 수 초가 걸리지만, RAP는 외부 인덱스로 키→(파일, 행 번호)를 직접 매핑해 의존적 read chain을 병렬화함으로써 이 문제를 해결했다. 기존 KV 스토어(Bigtable)와 달리 GB당 비용이 클라우드 스토리지 read 수준에 불과해 장기 이력 데이터나 롱테일 피처도 인터랙티브 조회 대상이 된다.

    #data-engineering#indexing#data-lake+2
  2. DB / 데이터·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    데이터 통합— MongoDB 원칙으로 document를 통합하고 동기화를 재설계하다 (3/3)

    여기어때 전시개발팀은 ES 단일 대문서 → MongoDB v1 RDB 1:1 파편화 → v2 도메인 단위 통합이라는 세 차례 구조 전환을 거쳐 전체 동기화 시간을 약 1시간에서 10분으로 단축했다. 이 글은 3부작의 마지막으로, v2 통합 문서 구조에 맞춰 "$set/$unset 필드 단위 부분 갱신 + 이벤트 발행" 기반 동기화를 어떻게 재설계했는지 구체적인 MongoDB 패턴과 함께 설명한다.

    #denormalization#data-sync#mongodb+2
  3. DB / 데이터·pinterest-engPinterest Engineering·

    Pinterest 차세대 DB 수집 프레임워크의 자동화된 스키마 진화

    Pinterest의 CDC 기반 DB 수집 파이프라인은 MySQL에서 Kafka, Flink, Spark, Iceberg를 거치는 다층 구조다. 스키마 변경이 생기면 모든 계층을 동시에 업데이트해야 해 드리프트, 배포 실패, 데이터 불일치가 반복됐다. Pinterest 엔지니어링 팀은 이를 해결하기 위해 가산적 변경만 자동화하는 스키마 진화 프레임워크를 구축하고, PR 기반 롤아웃과 SLA 기반 일관성 모델을 도입했다.

    #data-pipeline#apache-flink#cdc+2