TECH/sql

[Database] 데이터 무결성 vs 성능, 트랜잭션 격리 수준으로 결정한다 (Read Committed vs Repeatable Read)

런코리치 2026. 2. 23. 12:00

은행 앱에서 송금을 하거나, 쇼핑몰에서 마지막 남은 재고 1개를 두고 여러 명이 동시에 결제 버튼을 누를 때, 데이터베이스는 어떻게 오류 없이 정확한 값을 유지할까요? 그 비밀은 바로 트랜잭션(Transaction)과 격리 수준(Isolation Level)에 있습니다.


1. 트랜잭션(Transaction)이란?

트랜잭션은 데이터베이스의 상태를 변화시키는 하나의 논리적 작업 단위입니다. "전부 성공하든지, 아니면 전부 실패하든지(All or Nothing)"라는 원칙을 지킵니다.

💡 트랜잭션의 4가지 속성 (ACID)

  1. Atomicity(원자성): 트랜잭션 내의 작업은 하나로 묶여야 함.
  2. Consistency(일관성): 실행 전후의 데이터베이스 상태가 규칙을 위반하지 않음.
  3. Isolation(격리성): 실행 중인 트랜잭션에 다른 트랜잭션이 끼어들 수 없음.
  4. Durability(지속성): 성공적으로 완료된 결과는 영구적으로 기록됨.

2. 왜 '격리 수준'이 필요한가?

완벽한 격리(Isolation)를 위해서는 트랜잭션을 하나씩 순서대로 처리하면 됩니다. 하지만 그렇게 하면 동시 처리 속도가 너무 느려집니다.

따라서 성능(속도)과 정합성(정확도) 사이의 균형을 맞추기 위해 "어느 정도까지 간섭을 허용할 것인가?"를 결정하는 4단계의 격리 수준이 존재합니다.


3. ANSI 표준 격리 수준 4단계

① READ UNCOMMITTED (레벨 0)

  • 설명: 커밋되지 않은 데이터도 다른 트랜잭션이 읽을 수 있습니다.
  • 문제점: Dirty Read (아직 확정되지 않은 가짜 데이터를 읽음).
  • 예시: A가 송금 버튼을 눌러 잔액을 수정 중인데(아직 커밋 전), B가 그 수정 중인 금액을 조회합니다. 그런데 A가 송금을 취소(Rollback)해버리면 B는 존재하지 않는 데이터를 읽은 꼴이 됩니다.

② READ COMMITTED (레벨 1) - Oracle, PostgreSQL 기본값

  • 설명: 커밋된 데이터만 읽을 수 있습니다. 가장 대중적인 설정입니다.
  • 문제점: Non-Repeatable Read (한 트랜잭션 내에서 같은 쿼리를 두 번 보냈는데 값이 달라짐).
  • 예시: A가 통장 잔액(10만원)을 조회합니다. 그 사이 B가 5만원을 입금하고 커밋합니다. A가 다시 잔액을 조회하니 15만원이 나옵니다. 한 트랜잭션 안에서 조회 결과가 일관되지 않습니다.

③ REPEATABLE READ (레벨 2) - MySQL 기본값

  • 설명: 자신의 트랜잭션이 시작되기 전에 커밋된 내용만 읽습니다. 한 번 읽은 데이터는 트랜잭션 종료 시까지 동일함이 보장됩니다. (MVCC 기술 활용)
  • 문제점: Phantom Read (데이터가 없었는데 갑자기 나타남).
  • 예시: A가 '잔액 10만원 이상인 고객'을 조회합니다(3명). 그 사이 B가 새로운 고객(잔액 20만원)을 추가합니다. A가 다시 조회하니 4명이 나옵니다. 유령(Phantom)처럼 없던 데이터가 나타나는 현상입니다.

④ SERIALIZABLE (레벨 3)

  • 설명: 가장 엄격한 수준. 트랜잭션이 해당 영역의 데이터에 접근할 때 '읽기 잠금'을 걸어 다른 사용자의 수정을 원천 봉쇄합니다.
  • 특징: 정합성은 완벽하지만, 동시 처리 성능이 급격히 떨어집니다.

4. 실무에서 겪는 동시성 이슈와 해결책

🛒 시나리오: 한정판 운동화 선착순 결제

재고가 1개 남았을 때, 사용자 A와 B가 동시에 UPDATE를 시도합니다.

  1. A가 현재 재고 1개를 확인합니다.
  2. 동시에 B도 현재 재고 1개를 확인합니다.
  3. A가 결제 완료 후 재고를 0으로 수정합니다.
  4. B도 결제 완료 후 재고를 0으로 수정합니다. (이미 A가 샀는데!)

[해결책]

  • 오라클/PostgreSQL: SELECT ... FOR UPDATE 구문을 사용하여 조회 시점에 해당 행에 잠금(Lock)을 걸어 B가 아예 접근하지 못하게 대기시킵니다.
  • 낙관적 잠금(Optimistic Lock): 데이터 수정 시 버전 번호를 체크하여, 내가 읽은 버전과 다를 경우 수정을 거부합니다.

5. 마무리: 어떤 격리 수준을 선택해야 할까?

모든 서비스에 최고 수준인 SERIALIZABLE을 적용할 수는 없습니다. 시스템의 특성에 맞는 선택이 중요합니다.

  • 금융권/결제 시스템: 데이터 정합성이 최우선이므로 높은 격리 수준이나 FOR UPDATE와 같은 명시적 잠금을 적극 활용합니다.
  • SNS/커뮤니티: 좋아요 수나 조회수가 1~2개 오차가 나는 것은 큰 문제가 아니므로, 성능이 좋은 READ COMMITTED를 선호합니다.

격리 수준을 결정하는 것은 투자에서 '보안(Security)'과 '수익(Efficiency)' 사이의 균형을 맞추는 것과 같습니다. 격리가 완벽할수록 안전하지만 시스템은 느려지고, 너무 느슨하면 데이터가 오염되죠. 서비스 성격에 맞는 최적의 격리 수준을 선택하는 것, 그것이 바로 고급 개발자의 리스크 관리 능력입니다.