실무에서 겪은 DB isolation level
MySQL의 REPEATABLE READ에서 락 전에 읽은 잔액이 끝까지 고정돼 결제가 실패했다.
Oracle 기반에서 MySQL로 옮긴 뒤 신규 결제 시스템을 만들면서, 동일 유저의 동시 요청을 막기 위해 트랜잭션 안에서 DB lock을 잡고 잔액을 갱신했다. 업데이트는 balance = originalBalance 조건을 함께 걸어, 결제 이전 잔액과 같을 때만 반영되도록 설계했다.
문제는 락을 잡기 전에 readUserMethod(userId)로 잔액 존재 여부를 먼저 조회하고, 없으면 insert 하도록 추가한 뒤부터 시작됐다. 요청 A와 B가 동시에 들어오면 A가 먼저 락을 획득하고 결제를 끝내 커밋하더라도, B는 기다렸다가 들어간 뒤에도 A가 갱신한 최신 잔액이 아니라 처음 읽은 값을 계속 기준으로 삼았다.
원인은 MySQL의 기본 격리 수준인 REPEATABLE READ였다. 한 트랜잭션 안에서 먼저 읽은 값은 이후 다시 읽어도 유지되므로, 락보다 앞선 조회 결과가 트랜잭션 끝까지 고정돼 버린다. 반면 이전 시스템의 Oracle은 기본이 READ COMMITTED라서, 중간 커밋이 반영된 값을 다시 읽을 수 있었고 같은 문제가 드러나지 않았다.
해결책은 세 가지였다.
- 락을 먼저 잡은 뒤 잔액 조회와 insert를 수행해 커밋된 데이터만 보게 하기
- 해당 트랜잭션만
@Transactional(isolation = Isolation.READ_COMMITTED)로 바꾸기 - DB 전체의 isolation level을 READ COMMITTED로 조정하기
다만 REPEATABLE READ가 나쁜 것은 아니다. 예를 들어 결제 도중 상품 가격이 바뀌는 상황에서는, 중간에 갱신된 값을 다시 읽는 READ COMMITTED가 오히려 결제 실패를 늘릴 수 있다. 핵심은 격리 수준의 우열이 아니라, 서비스 정책과 동시성 요구에 맞게 선택해야 한다는 점이다.
또한 RDBMS마다 기본 격리 수준과 지원 범위가 다르므로, DB를 바꾸거나 프레임워크 설정을 손댈 때는 이 차이를 반드시 확인해야 한다.
이 요약은 원문 이해를 돕기 위한 큐레이션입니다. 저작권은 원저작자에게 있으며, 정확한 내용과 맥락은 원문을 확인하세요.
요약 오류, 출처 표기 문제, 삭제 요청은 문의 · 건의로 알려주세요.
