Write Skew가 Snapshot Isolation에서도 데이터 무결성을 깨뜨리는 이유
데이터베이스의 보이지 않는 함정 Write Skew를 이해하기
현대 서비스 개발에서 데이터의 정합성은 생존과 직결되는 문제입니다. 특히 금융, 커머스, 예약 시스템과 같이 동시에 여러 사용자가 데이터에 접근하는 환경에서는 더욱 그렇습니다. 많은 개발자가 데이터베이스의 격리 수준 중 하나인 스냅샷 격리(Snapshot Isolation, 이하 SI)를 사용하면 모든 동시성 문제가 해결될 것이라고 믿습니다. 하지만 SI에서도 데이터 무결성이 깨지는 치명적인 현상이 존재하는데, 이를 라이트 스큐(Write Skew)라고 부릅니다.
라이트 스큐는 단순히 데이터를 덮어쓰는 문제가 아니라, 데이터베이스가 허용한 규칙을 우회하여 논리적인 오류를 범하는 현상입니다. 이 가이드를 통해 라이트 스큐가 무엇인지, 왜 발생하는지, 그리고 이를 어떻게 방어할 수 있는지 실무적인 관점에서 상세히 알아보겠습니다.
스냅샷 격리 수준의 작동 원리와 한계
스냅샷 격리는 트랜잭션이 시작되는 시점의 데이터 상태를 스냅샷으로 찍어두고, 그 시점의 데이터만을 참조하는 방식입니다. 이를 통해 읽기 작업은 쓰기 작업을 방해하지 않으며, 동시에 여러 트랜잭션이 충돌 없이 빠르게 수행될 수 있습니다. 하지만 이 방식에는 치명적인 맹점이 있습니다.
SI는 서로 다른 트랜잭션이 ‘동일한 행(Row)’을 수정할 때만 충돌을 감지합니다. 즉, A 트랜잭션이 1번 행을 수정하고 B 트랜잭션이 2번 행을 수정한다면, 데이터베이스는 이 둘이 서로 다른 데이터에 영향을 준다고 판단하여 아무런 제약을 걸지 않습니다. 그러나 실제 비즈니스 로직에서는 1번과 2번 행의 값이 서로 밀접한 관계를 맺고 있는 경우가 많습니다. 바로 여기서 라이트 스큐가 발생합니다.
라이트 스큐가 발생하는 전형적인 시나리오
가장 이해하기 쉬운 예시는 병원 당직 시스템입니다. 병원에는 최소한 한 명의 의사가 당직을 서야 한다는 규칙이 있습니다. 현재 의사 A와 B가 모두 당직 중입니다. 이때 두 의사가 동시에 퇴근 신청을 합니다.
- 트랜잭션 1(의사 A): 현재 당직자가 2명임을 확인(A, B). 2명이므로 1명이 퇴근해도 1명이 남음. 퇴근 승인.
- 트랜잭션 2(의사 B): 현재 당직자가 2명임을 확인(A, B). 2명이므로 1명이 퇴근해도 1명이 남음. 퇴근 승인.
결과적으로 두 트랜잭션 모두 ‘당직자가 1명 이상이어야 한다’는 규칙을 만족한다고 판단했지만, 실제로는 두 명 모두 퇴근하여 당직자가 0명이 됩니다. SI 입장에서는 의사 A와 B의 상태를 각각 업데이트했으므로 충돌이 아니라고 보지만, 비즈니스 규칙은 완전히 파괴되었습니다. 이것이 바로 라이트 스큐의 본질입니다.
라이트 스큐와 일반적인 쓰기 충돌의 차이
많은 분이 라이트 스큐를 일반적인 쓰기 충돌과 혼동합니다. 다음 표를 통해 명확한 차이를 확인해 보세요.
| 구분 | 일반적인 쓰기 충돌 | 라이트 스큐(Write Skew) |
|---|---|---|
| 발생 원인 | 동일한 행을 동시에 업데이트 | 서로 다른 행을 업데이트하지만 논리적 제약 위반 |
| 감지 방식 | 데이터베이스 잠금(Lock)으로 자동 차단 | 데이터베이스가 정상 작업으로 인식 |
| 현상 | 데이터 유실 또는 오버라이트 | 비즈니스 규칙 위반(논리적 오류) |
라이트 스큐를 방지하는 실무적인 대응 전략
라이트 스큐는 데이터베이스의 격리 수준 설정만으로는 완벽하게 막기 어려운 경우가 많습니다. 따라서 다음과 같은 전략을 상황에 맞게 조합해야 합니다.
비관적 잠금 사용
가장 확실한 방법은 SELECT 문에 FOR UPDATE 구문을 사용하는 것입니다. 이를 사용하면 데이터를 읽는 시점부터 잠금을 걸어 다른 트랜잭션이 해당 데이터를 수정하지 못하게 합니다. 하지만 이 방법은 성능 저하를 유발할 수 있으므로, 꼭 필요한 구간에만 좁게 적용해야 합니다.
직렬화 가능 격리 수준 활용
데이터베이스에서 제공하는 가장 높은 격리 수준인 Serializable을 사용하면 라이트 스큐를 원천 차단할 수 있습니다. 하지만 이는 성능 비용이 매우 크고 데드락 발생 확률이 높아지므로, 처리량이 많은 시스템에서는 신중한 선택이 필요합니다.
애플리케이션 레벨의 제약 조건 강화
데이터베이스가 해결해주지 못한다면 애플리케이션에서 방어 코드를 작성해야 합니다. 예를 들어, 당직 시스템의 경우 당직자 수를 저장하는 별도의 ‘카운터 테이블’을 만들고, 이 카운터를 업데이트할 때 원자적(Atomic)으로 처리되도록 설계하는 방식입니다. 이렇게 하면 두 트랜잭션이 동일한 카운터 행을 수정하게 되므로, 데이터베이스의 기본 잠금 메커니즘이 작동하여 라이트 스큐를 막을 수 있습니다.
흔한 오해와 전문가의 조언
가장 흔한 오해 중 하나는 “트랜잭션 내에서 SELECT 후 UPDATE를 하면 안전하다”는 생각입니다. 하지만 SI 환경에서는 트랜잭션 시작 시점의 스냅샷을 읽기 때문에, 그 이후 다른 트랜잭션이 변경한 값은 반영되지 않습니다. 즉, 내 눈에는 데이터가 변하지 않은 것처럼 보여도 실제로는 이미 변경되었을 수 있다는 점을 항상 기억해야 합니다.
데이터베이스 전문가들은 “데이터 정합성은 데이터베이스 엔진뿐만 아니라 비즈니스 로직의 설계 단계에서부터 고려되어야 한다”고 강조합니다. 단순히 기술적인 격리 수준에 의존하기보다, 데이터 간의 의존성을 파악하고 이를 원자적으로 처리할 수 있는 구조를 만드는 것이 훨씬 비용 효율적이고 안전합니다.
비용 효율적인 활용을 위한 체크리스트
시스템을 설계할 때 다음 체크리스트를 활용하여 라이트 스큐 위험을 최소화하세요.
- 현재 로직이 ‘읽기 후 쓰기’ 형태인가?
- 그 읽기 대상이 되는 데이터가 다른 트랜잭션의 쓰기 대상과 논리적으로 연결되어 있는가?
- 데이터베이스의 격리 수준이 Snapshot Isolation(또는 PostgreSQL의 Repeatable Read)인가?
- 만약 그렇다면, 공유 자원(카운터, 상태값)을 별도로 관리하여 물리적 충돌을 유도할 수 있는가?
위 항목 중 하나라도 해당한다면 라이트 스큐 발생 가능성이 높습니다. 이럴 때는 성능 저하를 감수하고 잠금을 걸 것인지, 아니면 구조를 변경하여 원자성을 보장할 것인지 결정해야 합니다.
자주 묻는 질문과 답변
Q: Serializable을 쓰면 모든 문제가 해결되지 않나요?
A: 이론적으로는 그렇습니다. 하지만 Serializable은 트랜잭션 간의 충돌을 감지하여 롤백시키는 경우가 빈번합니다. 이로 인해 애플리케이션에서 재시도(Retry) 로직이 대량으로 발생하면 전체적인 시스템 처리량(Throughput)이 급격히 떨어질 수 있습니다.
Q: 라이트 스큐를 무조건 막아야 하나요?
A: 아닙니다. 모든 데이터가 엄격한 정합성을 요구하지는 않습니다. 예를 들어, SNS의 ‘좋아요’ 숫자나 조회수 등은 약간의 오차가 발생해도 비즈니스에 큰 지장을 주지 않는 경우가 많습니다. 반면 결제, 재고, 예약 시스템은 라이트 스큐에 매우 취약하므로 반드시 방어해야 합니다.
Q: 분산 데이터베이스 환경에서는 더 위험한가요?
A: 네, 분산 환경에서는 여러 노드에 걸쳐 데이터가 분산되므로 단일 데이터베이스보다 라이트 스큐를 감지하고 처리하기가 훨씬 까다롭습니다. 분산 트랜잭션 모델을 도입하거나, 이벤트 소싱과 같은 아키텍처를 고려하는 것이 좋습니다.
데이터베이스의 기술적 특성을 깊이 이해하는 것은 단순히 버그를 막는 것을 넘어, 더 견고하고 확장 가능한 시스템을 만드는 기초가 됩니다. 라이트 스큐를 이해했다면, 이제 여러분의 코드에서 데이터의 논리적 흐름이 어디서 꼬일 수 있는지 다시 한번 점검해 보시기 바랍니다.
댓글 0
첫 댓글을 남겨보세요.