데이터베이스의 분실 갱신(Lost Update) 원인 분석
데이터베이스에서 여러 트랜잭션이 동시에 실행될 때 발생하는 ‘분실 갱신(Lost Update)’은 대표적인 동시성 문제입니다. 이는 여러 사용자가 같은 데이터를 동시에 수정하려 할 때, 하나의 작업 결과가 다른 작업에 의해 덮어쓰여 결과적으로 데이터 변경이 유실되는 현상을 말합니다.
이번 포스트에서는 분실 갱신이 발생하는 핵심적인 원인과 그 배경에 대해 담담한 어투로 정리해 보겠습니다.
1. 동시성 환경과 경쟁 상태 (Race Condition)
분실 갱신은 경쟁 상태(Race Condition)의 전형적인 예시 중 하나입니다. 동시 처리 환경에서 데이터의 원자성(Atomicity)이 보장되지 않을 때 주로 발생합니다.
- 서버가 한 대이더라도 Spring Boot의 멀티스레드나 NestJS의 비동기 구조에서는 수십, 수백 개의 트랜잭션이 동시에 같은 데이터에 접근할 수 있습니다.
- 이처럼 여러 트랜잭션이 동일한 데이터를 두고 경쟁할 때, 실행 순서나 타이밍에 따라 결과가 달라지는 상황을 ‘경쟁 상태’라고 합니다. 분실 갱신은 이러한 경쟁 상태가 야기하는 문제입니다.
2. ‘읽기-수정-쓰기’ 과정의 허점
분실 갱신은 트랜잭션이 데이터를 ‘읽고(Read)’, 그 값을 기반으로 ‘수정(Modify)’한 후, 다시 ‘쓰는(Write)’ 과정에서 발생합니다. 데이터를 읽은 시점과 쓰는 시점 사이에 다른 트랜잭션이 개입하면, 처음 읽었던 데이터의 일관성이 깨지게 됩니다.
상품 재고를 줄이는 상황을 예로 들어보겠습니다.
- 초기 상태: 상품 재고(stock)는 1개입니다.
- 트랜잭션 1 (Tx1): 재고를 조회합니다. (현재 stock = 1)
- 트랜잭션 2 (Tx2): 거의 동시에 재고를 조회합니다. (현재 stock = 1)
- 트랜잭션 1 (Tx1): 조회한 값을 기반으로 재고를 1 줄이고 커밋합니다. (데이터베이스의 stock = 0)
- 트랜잭션 2 (Tx2): 이전에 읽었던 stock = 1 값을 기준으로 재고를 1 줄이고 커밋합니다. (데이터베이스의 stock은 다시 0이 됨)
이 시나리오에서는 두 번의 주문으로 재고가 -1이 되어야 하지만, 두 번째 트랜잭션이 첫 번째 트랜잭션의 변경 사항을 무시하고 덮어썼기 때문에 최종 재고는 0으로 남습니다. 결국, 첫 번째 갱신(Update) 내용은 사라지게 됩니다.
3. 방어 장치의 부재
분실 갱신은 다음과 같이 적절한 방어 장치가 없는 환경에서 쉽게 발생합니다.
- 락(Lock)의 부재: 데이터를 읽은 후 수정하기 전까지 다른 트랜잭션의 접근을 막지 못하면 분실 갱신이 발생하기 쉽습니다.
- 낮은 트랜잭션 격리 수준: 트랜잭션의 경계가 불분명하거나,
READ COMMITTED와 같이 낮은 격리 수준에서는 다른 트랜잭션이 커밋한 내용을 읽을 수 있어 분실 갱신이 발생할 수 있습니다.
결론적으로 분실 갱신을 방지하기 위해서는 트랜잭션의 범위를 명확히 설정하고, 데이터의 원자성을 보장해야 합니다. 이를 위해 비관적 락(Pessimistic Lock)이나 낙관적 락(Optimistic Lock), 또는 조건부 UPDATE와 같은 기법을 활용하여 데이터의 일관성을 유지하는 것이 중요합니다.