[DB] 정합성을 지키는 전략 (MySQL 트랜잭션 격리 수준 + 낙관적 락)

조시현·2025년 4월 23일
0
post-thumbnail

1. 트랜잭션 격리 수준(Isolation Level) 이란?

🧱 트랜잭션(Transaction)과 격리 수준(Isolation)의 개념

  • 트랜잭션(Transaction): 데이터베이스에서 하나의 작업 단위를 의미하며, 다음의 4가지 특성(ACID)을 만족해야 합니다.

    • Atomicity (원자성): 모두 수행되거나 모두 수행되지 않아야 함
    • Consistency (일관성): 트랜잭션 완료 후에도 데이터는 항상 유효한 상태
    • Isolation (격리성): 각 트랜잭션은 서로 간섭하지 않아야 함
    • Durability (지속성): 일단 완료된 트랜잭션은 시스템 장애에도 유지됨
  • Isolation Level은 여러 트랜잭션이 동시에 실행될 때, 서로의 작업이 어느 정도까지 보장되며 간섭이 허용되는가를 정의합니다.


🕸️ 왜 격리 수준이 필요한가? (동시성 문제)

트랜잭션이 격리되지 않으면 다음과 같은 데이터 정합성 문제가 발생할 수 있습니다:

  • Dirty Read: 아직 커밋되지 않은 데이터를 읽는 문제
  • Non-Repeatable Read: 같은 쿼리를 반복해도 결과가 바뀌는 문제
  • Phantom Read: 조건에 맞는 새로운 행이 삽입되거나 삭제되는 문제

이러한 문제를 방지하기 위한 것이 격리 수준이며, 그 강도에 따라 위 문제들을 방지할 수 있는 범위가 달라집니다.


2. 4가지 Isolation Level 비교 요약

🔹 1) READ UNCOMMITTED

  • 가장 낮은 격리 수준
  • 다른 트랜잭션이 커밋하지 않은 변경사항도 읽을 수 있음 (Dirty Read 허용)
  • 빠르지만 정합성 보장이 거의 없음

🔹 2) READ COMMITTED

  • 대부분의 DBMS에서 기본 설정 (MySQL은 아님)
  • 커밋된 데이터만 읽을 수 있음 → Dirty Read 방지
  • 하지만 Non-Repeatable Read는 발생 가능

🔹 3) REPEATABLE READ

  • MySQL의 기본 격리 수준
  • 동일한 쿼리에 대해 항상 같은 결과를 반환 → Non-Repeatable Read 방지
  • 단, Phantom Read는 방지하지 못함

🔹 4) SERIALIZABLE

  • 가장 높은 격리 수준
  • 모든 트랜잭션을 순차적으로 처리한 것처럼 보이도록 보장
  • 모든 읽기 문제를 방지하지만, 성능 저하가 심함

📌 ANSI SQL 표준 vs MySQL의 차이점

  • ANSI SQL 표준에서는 REPEATABLE READ가 Phantom Read까지 막는다고 정의되어 있다.
  • 그러나 MySQL의 InnoDB 스토리지 엔진에서는 Phantom Read가 발생할 수 있다
    • 이유: 내부적으로 MVCC로 구현되어 있으며, 범위 조건 INSERT/DELETE는 막지 않는다

즉, MySQL의 REPEATABLE READ는 이름과 달리 완전한 격리 수준이 아님에 주의해야 합니다.

3. 격리 수준만으로 부족한 이유: 읽기 vs 쓰기 관점에서의 한계

📖 읽기 관점에서 살펴보는 Isolation Level

격리 수준은 주로 읽기 충돌을 방지하기 위해 도입되었으며, 다음과 같은 문제가 발생할 수 있습니다:

👓 발생 가능한 읽기 문제 요약

문제 유형설명발생 조건
Dirty Read다른 트랜잭션의 미커밋 데이터를 읽음READ UNCOMMITTED
Non-Repeatable Read같은 SELECT가 다른 결과를 반환READ COMMITTED
Phantom ReadWHERE 조건에 해당하는 row가 뒤에서 추가됨REPEATABLE READ (in MySQL)

📊 어떤 Isolation Level이 어떤 문제를 막아주는가?

격리 수준Dirty ReadNon-Repeatable ReadPhantom Read
READ UNCOMMITTED발생함발생함발생함
READ COMMITTED방지됨발생함발생함
REPEATABLE READ방지됨방지됨발생 가능 (MySQL 기준)
SERIALIZABLE방지됨방지됨방지됨

💡 읽기 충돌만 본다면 SERIALIZABLE이 유일하게 모든 문제를 막지만, 성능 측면에서는 실무 적용이 어렵습니다.


쓰기 관점에서 살펴보는 Isolation Level

읽기 외에도 더 치명적인 문제가 발생하는 영역이 쓰기 충돌입니다.

🔥 쓰기 충돌의 3가지 주요 유형

유형설명발생 시점
Write Skew서로 다른 조건을 보고 동시에 쓰기를 시도 → 정합성 깨짐읽기 후 쓰기 사이
Lost Update마지막 트랜잭션이 이전 값을 덮어씀동시에 같은 row를 수정
Write-Write Conflict동일 데이터를 동시에 수정하려는 시도비관적 락이나 SERIALIZABLE 없으면 발생

❓ 격리 수준만으로 충분한가?

절대 아닙니다.

  • READ COMMITTED, REPEATABLE READ 모두 쓰기 충돌을 자동으로 감지하거나 막지 못함
  • 특히 REPEATABLE READ는 읽기는 보호하지만, 쓰기 간 충돌은 허용함 → 대표적인 예: Write Skew

그래서 실무에서는 락 (비관적/낙관적) 전략이 필요합니다.


4. MySQL의 기본 격리 수준: Repeatable Read의 한계

MySQL은 기본적으로 Repeatable Read (RR) 를 사용하며, 이는 다음과 같은 특성을 가집니다:

✅ 장점: 일관된 읽기 제공 (Consistent Read)

  • 트랜잭션 시작 시점의 Snapshot을 사용하여 항상 동일한 값을 읽음
  • Non-Repeatable Read는 방지됨

❌ 단점: 쓰기 충돌 감지는 불가

  • RR은 읽기 시점만 보장 → 쓰기에는 개입하지 않음
  • 트랜잭션이 동일한 값을 보고 서로 다른 쓰기를 해도 이를 감지 못함

→ 결국 Write Skew 문제가 발생할 수 있는 구조


5. 실무 사례: 입금/잔고 시스템에서 발생하는 문제

💸 문제 시나리오

  1. 사용자 A와 B가 동시에 계좌 잔고를 확인 (예: balance = 10000원)
  2. 각각 5000원을 입금하기로 결정
  3. 둘 다 balance += 5000 수행
  4. 최종 잔고는 15000원이 되어야 함

하지만 실제로는 마지막 트랜잭션만 반영되어 10000 → 15000 또는 10000 → 20000이 됨

→ 이게 바로 Lost Update or Write Skew 현상


🧨 정합성 손상의 결과

  • 실제 입금은 잘 됐다고 사용자에게 보여졌지만, DB에는 반영되지 않음
  • 충돌 자체를 감지하지 못하므로, 오류 로그도 남지 않음
  • 이로 인해 사용자 A 또는 B가 돈을 입금했지만 사라진 것처럼 보이게 됨

📌 MySQL Repeatable Read는 읽기 일관성은 지켜주지만, 쓰기 정합성까지는 보장하지 않음

📌 그 이유는 MySQL이 MVCC 기반으로 동작하면서, 트랜잭션 시작 시점의 Snapshot만을 기준으로 읽기 정합성만 보장하기 때문입니다.
쓰기 시점에 데이터가 변경됐는지는 검사하지 않기 때문에, 동일 데이터를 기반으로 한 동시 쓰기 충돌을 감지하지 못하며,
이로 인해 balance 같은 누적 연산에서는 정합성 오류(Lost Update, Write Skew) 가 발생할 수 있습니다.
따라서 안정적인 처리를 위해서는 낙관적 락(Optimistic Locking) 또는 비관적 락(Pessimistic Locking) 을 추가 적용해야 합니다.


6. 실무에서 쓰기 충돌을 다루는 3가지 전략

단순한 락이 아니다 — 정합성과 신뢰를 수호하는 실전 기술


🔐 ① 비관적 락 (Pessimistic Locking: SELECT ... FOR UPDATE)

  • 트랜잭션 내에서 데이터를 읽을 때 다른 트랜잭션이 해당 row를 수정하지 못하도록 잠금
  • 가장 강력하고 확실한 방식
  • 단점: Deadlock 가능성, 높은 경쟁 시 성능 저하
SELECT balance FROM account WHERE id = 1 FOR UPDATE;

트랜잭션 커밋 전까지 다른 트랜잭션은 이 row에 접근 불가


🧱 ② 테이블 락 (Table Lock)

  • 테이블 전체를 잠그는 방식
  • 단점: 병렬성 완전 손실, 거의 쓰이지 않음
  • 보통 DDL 수행 시 내부적으로 사용됨
LOCK TABLE account WRITE;

✨ ③ 낙관적 락 (Optimistic Locking using version)

  • 실제 충돌이 일어날 거라고 낙관적으로 가정
  • 데이터를 업데이트할 때 버전(version)이나 수정 시점(timestamp) 을 조건으로 넣어 충돌 감지
UPDATE account
SET balance = balance + 5000, version = version + 1
WHERE id = 1 AND version = 5;

충돌 시 업데이트 실패 → 이를 감지하고 재시도할 수 있음


7. 낙관적 락이 실무에서 표준이 된 이유

✅ 가장 가볍고 유연한 락 전략

  • DB 내부의 락이 아님 → 애플리케이션 수준의 충돌 감지
  • 락 경합 없음 → 대량의 동시 요청도 병렬로 처리 가능
  • 충돌이 거의 없는 환경 (예: 잔고 조회, 알림 설정 등)에 최적화

🔁 충돌이 감지되면 어떻게 되는가?

  • 업데이트가 실패하면, 이를 애플리케이션에서 감지 가능
  • 실패 로직을 재시도하거나 사용자에게 알림으로 처리 가능
if (updateResult == 0) {
    // 충돌 발생 → 재시도 혹은 예외 처리
}

📈 확장성과 정합성 사이의 완벽한 균형

  • 무거운 락 없이도 정합성 보장
  • MSA, CQRS, 이벤트 소싱 같은 분산 시스템에서도 효과적으로 사용 가능

8. 낙관적 락은 기술이 아닌 비즈니스 정합성 수호 전략

🧭 "락을 건다"는 건 기술이 아니라 의지

  • 잔고, 포인트, 재고 같은 값은 '절대 틀리면 안 되는 값'
  • 낙관적 락은 그 값을 절대 틀리지 않게 하겠다는 비즈니스 의지의 표현

🧱 클린 아키텍처에서의 위치

  • 낙관적 락은 도메인 계층의 불변 조건을 지키기 위한 도구
  • DB나 기술 스택에 의존하는 것이 아니라, 비즈니스 규칙으로써 존재

즉, 낙관적 락은 인프라가 아니라 도메인의 책임


🔄 예시: 계좌 이체 시스템

  • 동시에 2명의 사용자가 같은 잔고에 접근할 수 있음
  • 낙관적 락으로 충돌을 감지하고 막음 → 사용자의 돈이 잘못 계산되지 않도록 보장

💡 낙관적 락은 단순한 성능 전략이 아닌, 정합성과 신뢰를 위한 최종 방어선입니다.

시스템이 커질수록, 낙관적 락을 어디에 걸 것인지를 도메인 중심으로 고민해야 합니다.

9. 각 DBMS별 기본 Isolation Level 정리

DBMS기본 격리 수준특이사항
MySQL (InnoDB)Repeatable ReadMVCC 기반, Phantom Read 일부 허용
PostgreSQLRead Committed가장 현실적인 기본값, 강력한 MVCC
OracleRead CommittedSerializable은 Serializable Snapshot 방식으로 작동
SQL ServerRead CommittedLock 기반 격리 방식, Snapshot 격리 옵션 필요

🔎 특징 요약

  • MySQL은 Repeatable Read를 기본으로 하지만, 실제로는 Phantom Read까지 막지 않음 (MVCC의 한계)
  • PostgreSQL은 기본이 Read Committed이지만 MVCC로 매우 강력한 일관성 유지
  • Oracle은 Serializable이어도 실제로는 Snapshot 기반 → 동시성 뛰어남
  • SQL Server는 기본적으로 락 기반 → 성능 튜닝에 민감

📌 DBMS별 기본 정책이 다르기 때문에, 동일한 트랜잭션 로직이라도 DBMS에 따라 격리 수준 설정을 명시하는 것이 중요


📌 다음 이야기 예고

지금까지 우리는 트랜잭션 격리 수준과 실무에서 발생하는 쓰기 충돌 문제, 그리고 이를 해결하는 전략들에 대해 살펴보았습니다.
하지만 트랜잭션 레벨에서의 정합성 제어만으로는 부족할 수 있습니다.

👉 더 나아가면, 아키텍처 레벨에서 읽기와 쓰기를 분리하여 확장성과 정합성을 동시에 확보하는 전략으로 이어질 수 있습니다.
예를 들어, CQRS와 Redis를 활용한 분산 환경에서의 설계 방식이 그러한 사례 중 하나입니다.


이러한 주제는 트랜잭션 격리 수준의 한계를 넘어서서,
시스템 확장성과 고가용성을 고려해야 하는 환경에서 점점 중요해지고 있습니다.

기회가 된다면 다음 글에서는 아키텍처적 접근을 실전 사례와 함께 다뤄보겠습니다.

profile
Luck favors the prepared. Chance favors the prepared mind

0개의 댓글