데이터베이스 동시성 문제 해결을 위한 핵심 전략
초당 수천 건 이상의 요청이 몰리는 서비스에서는 단일 데이터베이스 인스턴스로 읽기 부하를 감당하기 어렵습니다. 이때 읽기 스케일 아웃(Read Scale-out) 전략이 사용되지만, 복제 지연 문제에 대한 대응이 필요합니다.
데이터베이스 동시성 문제는 서버가 한 대이더라도 멀티스레드나 비동기 처리 구조로 인해 여러 트랜잭션이 동일한 데이터에 동시에 접근하면서 발생할 수 있습니다. 이러한 동시성 문제를 해결하고 데이터의 정합성, 성능, 확장성의 균형을 맞추는 것은 안정적인 시스템 설계의 핵심입니다.
주요 동시성 문제(Race Condition, Deadlock 등)를 해결하기 위한 핵심 전략과 기술들을 소개합니다.
1. 트랜잭션 격리 수준 (Transaction Isolation Level) 설정 전략
트랜잭션 격리 수준은 여러 요청이 동시에 같은 데이터를 다룰 때 데이터 정합성이 깨지는 것을 방지하는 중요한 설정입니다. 격리 수준이 높을수록 데이터의 정확성은 보장되지만, 동시 처리 성능(TPS)은 저하되고 교착 상태(Deadlock) 발생 가능성이 커집니다.
| 격리 수준 | 허용하는 이상 현상 | 주요 특징 및 권장 사용처 |
|---|---|---|
| READ COMMITTED (RC) | Non-Repeatable Read, Phantom Read 허용 | Dirty Read를 방지하며, 커밋된 데이터만 조회합니다. Oracle, PostgreSQL의 기본값이며, 응답 속도가 중요하고 데이터가 약간 변경되어도 치명적이지 않은 일반적인 서비스에 적합합니다. |
| REPEATABLE READ (RR) | Phantom Read 허용 | 트랜잭션 내에서 조회한 데이터의 일관성을 보장하여 Non-Repeatable Read를 방지합니다. 재고, 잔액처럼 “읽고 바로 수정”하는 로직에 적합하며, 돈, 재고, 쿠폰 등 정확성이 필수적인 핵심 데이터 처리에 권장됩니다. |
| SERIALIZABLE (SR) | 모든 이상 현상 차단 | 가장 엄격한 격리 수준으로, 모든 트랜잭션을 순차적으로 실행한 것과 동일한 결과를 보장합니다. TPS가 크게 저하되고 Deadlock 위험이 높아, 은행 이체나 회계 마감처럼 오차를 허용할 수 없는 극히 제한적인 상황에서 사용됩니다. |
격리 수준 선택 기준:
- 정합성 중요도: 금융 정보, 재고와 같이 민감한 데이터는 REPEATABLE READ 이상의 격리 수준을 사용하는 것이 안전합니다.
- 경합 빈도: 동일한 데이터에 대한 접근이 잦다면, READ COMMITTED 격리 수준과 비관적 락을 조합하는 방식이 효과적일 수 있습니다.
2. 데이터베이스 락 (Lock) 전략
락(Lock)은 여러 작업이 동시에 데이터에 접근할 때 정합성을 유지하기 위한 핵심 제어 장치입니다.
비관적 락 (Pessimistic Lock)
- 철학: 충돌이 발생할 가능성이 높다고 가정하고, 데이터 접근 시점에 미리 잠금을 설정하여 안전을 확보합니다.
- 기술: 트랜잭션 시작 시 Exclusive Lock (X-lock)을 설정하여 다른 트랜잭션의 접근을 차단합니다.
- 구현:
SELECT * FROM stock WHERE id = 1 FOR UPDATE;와 같은 SQL 구문이나 Spring JPA의@Lock(PESSIMISTIC_WRITE)어노테이션을 사용합니다. - 장점: 데이터 충돌 가능성을 원천적으로 차단하여 안전하게 순차 처리를 보장합니다.
- 단점: 락 대기 시간으로 인해 전체 처리 속도가 저하될 수 있으며, Deadlock 발생 확률이 높아집니다.
- 적합 환경: 상품 재고 차감, 좌석 예약 등 동시 처리 시 문제가 발생하는 충돌 빈도가 높은 작업에 적합합니다.
낙관적 락 (Optimistic Lock)
- 철학: 충돌이 드물다고 가정하고, 일단 작업을 진행한 뒤 커밋 시점에 충돌 여부를 확인합니다.
- 기술: 트랜잭션 동안 락을 걸지 않고, 커밋 직전에 버전(version) 필드나 조건부 업데이트를 통해 데이터가 변경되지 않았는지 검사합니다.
- 버전 필드를 사용하는 경우,
UPDATE ... WHERE id = ? AND version = ?과 같은 조건으로 데이터를 수정하고 성공 시 버전을 1 증가시킵니다.
- 버전 필드를 사용하는 경우,
- 장점: 락 대기가 없어 초당 처리량(TPS)이 높고, Deadlock 발생 위험이 없습니다.
- 단점: 충돌 발생 시 재시도 로직을 구현해야 하며, 충돌이 잦은 환경에서는 오히려 비효율적일 수 있습니다.
- 적합 환경: 데이터 충돌이 드물거나 분산 환경과 같이 데이터베이스 락만으로 동시성 제어가 어려운 경우에 유용합니다.
3. Deadlock (교착 상태) 예방 및 대처 전략
Deadlock은 두 개 이상의 트랜잭션이 서로 상대방의 락이 해제되기를 기다리며 무한 대기 상태에 빠지는 문제입니다. Deadlock은 TPS를 급격히 떨어뜨리고 시스템 장애의 원인이 될 수 있습니다.
| 전략 | 설명 |
|---|---|
| 락 획득 순서 일관되게 고정 | 모든 트랜잭션이 동일한 순서로 리소스에 접근하도록 설계하여 Deadlock 발생의 근본 원인 중 하나인 순환 대기를 방지합니다. |
| 트랜잭션을 최대한 짧게 유지 | 처리 로직을 빠르게 완료하고, 외부 API 호출이나 파일 I/O와 같은 오래 걸리는 작업은 트랜잭션 범위 밖으로 분리하여 락 보유 시간을 최소화합니다. |
| 자동 재시도 로직 구현 | 충돌이나 Deadlock 관련 오류 발생 시, @Retryable (Java)나 p-retry (NestJS/TS) 같은 라이브러리를 활용하여 일정 횟수만큼 자동으로 재시도하도록 구성합니다. |
| DB 타임아웃 튜닝 | MySQL의 innodb_lock_wait_timeout과 같은 설정을 적절히 조정하여, 일정 시간 이상 대기하는 트랜잭션이 자동으로 실패 처리되도록 유도합니다. |
4. DB 제약 조건 및 원자적 연산 (Atomic Operations) 활용
애플리케이션 코드의 버그가 발생하더라도 데이터베이스가 최후의 방어선 역할을 할 수 있도록 설계해야 합니다.
- 조건부 UPDATE 단일 쿼리:
UPDATE product SET stock = stock - 1 WHERE id = :pid AND stock >= 1;처럼 WHERE 절에 조건을 추가하여 쿼리 자체가 원자적 연산이 되도록 합니다. 이 방식은 트랜잭션을 단축시켜 TPS 효율을 높이는 데 효과적입니다. - DB 유니크 제약 활용: 쿠폰 중복 사용 방지와 같은 시나리오에서
CREATE UNIQUE INDEX ux_coupon ON coupon_usage(user_id, coupon_id);와 같은 Unique 제약 조건을 설정하면 데이터베이스 레벨에서 중복을 원천적으로 차단할 수 있습니다. - 멱등성 제약 설계: 트랜잭션 격리 수준이나 락 전략 외에도 멱등성 처리나 중복 방지 토큰과 같은 기법을 종합적으로 활용하여 시스템의 안정성을 높여야 합니다.
5. 실무 판단 흐름 요약
실제 시스템에서는 단일 전략에 의존하기보다, 비관적 락과 낙관적 락을 혼용하거나, 락, DB 제약, 캐시 등 다양한 기술을 복합적으로 고려하여 최적의 균형점을 찾아야 합니다.
| 상황 | 권장 전략 |
|---|---|
| 경합이 자주 일어남 | 비관적 락 (SELECT FOR UPDATE)을 사용하여 선제적으로 접근을 막고 안정성을 확보합니다. |
| TPS (초당 처리량)가 중요함 | 낙관적 락을 기본으로 채택하고, 충돌 발생 시 재시도 로직으로 대응합니다. |
| 분산 환경/서버가 여러 대 | 단일 DB 락만으로는 부족하므로, Redis Redlock과 같은 분산 락 도입을 검토합니다. |
| 재고/잔액/쿠폰 등 핵심 자원 제어 | 높은 격리 수준(RR 이상)과 X-Lock (비관적 락) 또는 조건부 UPDATE 패턴을 활용하여 데이터 정합성을 철저히 보장합니다. |