재고 차감 — 비관적 락
상황
주문이 동시에 들어오면 여러 요청이 재고를 동시에 읽고 차감한다. 재고가 1개 남은 상품에 100명이 동시에 주문하면 모두 재고가 1개라고 읽은 뒤 차감을 시도해서 재고가 마이너스가 되는 문제가 생긴다.
낙관적 락을 쓰면?
재고 차감은 동시 주문이 몰릴 때 충돌이 거의 확실하게 발생한다. 낙관적 락은 충돌이 나면 재시도를 하는데, 100명이 동시에 요청하면 대부분의 요청이 충돌 → 재시도 → 또 충돌을 반복하면서 재시도가 폭발적으로 증가한다. 결국 성능이 오히려 더 나빠진다.
Redis 분산락을 쓰면?
재고는 DB에 저장되는 데이터다. Redis 분산락으로 락을 잡고 DB에서 재고를 읽고 차감하는 구조가 되는데, 이렇게 하면 Redis → DB 두 번의 네트워크 비용이 발생하고 Redis 장애 시 재고 차감 자체가 불가능해진다. 재고는 DB 레벨에서 직접 락을 거는 게 더 안전하다.
비관적 락을 선택한 이유
재고 차감은 충돌이 확실한 구간이고, 반드시 하나만 성공해야 한다. DB 레벨에서 SELECT FOR UPDATE로 행을 잠그면 다른 요청은 락이 풀릴 때까지 대기하고, 순서대로 정확하게 차감된다. 처리량이 줄어드는 단점이 있지만 재고 정합성이 100% 보장된다는 게 더 중요한 상황이다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM ProductItem p WHERE p.id = :id")
ProductItem findByIdWithLock(@Param("id") Long id);
포인트 차감 — 낙관적 락
상황
주문 시 포인트를 차감하거나 배송완료 시 포인트를 적립할 때 동시성 문제가 발생할 수 있다. 같은 사용자의 포인트를 동시에 두 곳에서 차감하면 잔액이 맞지 않는 문제가 생긴다.
비관적 락을 쓰면?
포인트는 한 사용자가 동시에 여러 곳에서 차감 요청을 보낼 가능성이 낮다. 충돌 가능성이 낮은 상황에서 매번 DB 행을 잠그면 불필요한 대기가 발생하고 성능이 떨어진다. 재고처럼 수백 명이 동시에 같은 데이터를 건드리는 상황이 아니기 때문에 오버헤드가 크다.
Redis 분산락을 쓰면?
포인트는 사용자별로 독립된 데이터다. Redis 분산락까지 쓰는 건 구현 복잡도만 높아지고 실익이 없다. Redis 장애 시 포인트 차감이 불가능해지는 리스크도 생긴다.
낙관적 락을 선택한 이유
충돌 가능성이 낮은 상황에서는 락 없이 읽고 쓰되, 충돌이 났을 때만 재시도하는 낙관적 락이 적합하다. @Version 컬럼으로 충돌을 감지하고, 충돌 시 @Retryable로 최대 3회 재시도한다. 평소 트래픽에서는 락 비용 없이 빠르게 처리되고, 드물게 충돌이 나도 재시도로 처리된다.
@Entity
public class Point {
@Version
private int version;
}
@Retryable(value = ObjectOptimisticLockingFailureException.class, maxAttempts = 3)
public void usePoint(Long userId, int amount) { ... }
쿠폰 발급 — Redis 분산락
상황
선착순 쿠폰 발급에서 수천 명이 동시에 같은 쿠폰을 발급받으려고 요청한다. 발급 가능 수량이 100장인데 동시에 1000명이 요청하면 중복 발급이 발생해서 100장을 초과할 수 있다.
비관적 락을 쓰면?
DB 비관적 락은 단일 서버에서는 유효하다. 하지만 서버가 여러 대인 환경에서는 각 서버의 DB 커넥션이 다르기 때문에 락이 서버 간에 공유되지 않는다. 서버 A에서 락을 잡아도 서버 B에서 동시에 락을 잡을 수 있어서 중복 발급이 발생한다. 또한 수천 명이 동시에 요청할 때 DB 락으로 직렬화하면 DB 커넥션 풀이 고갈될 위험이 있다.
낙관적 락을 쓰면?
쿠폰 발급은 충돌이 확실하게 발생하는 상황이다. 수천 명이 동시에 같은 쿠폰의 issued_quantity를 읽고 수정하려 하기 때문에 대부분의 요청이 충돌 → 재시도를 반복한다. 재시도가 폭발적으로 증가해서 오히려 서버에 더 큰 부하를 준다.
Redis 분산락을 선택한 이유
Redis의 SETNX 명령어는 원자적으로 실행되기 때문에 여러 서버 인스턴스가 동시에 실행되는 환경에서도 하나의 요청만 락을 획득할 수 있다. DB에 직접 락을 걸지 않아서 DB 부하도 줄어들고, 서버 간 락 공유도 보장된다. 쿠폰 재고를 Redis에 DECR로 관리하면 원자적 차감까지 가능해서 DB 조회 없이 빠르게 수량 제어가 된다.
RLock lock = redissonClient.getLock("lock:coupon:" + couponId);
try {
if (lock.tryLock(1, 3, TimeUnit.SECONDS)) {
// 쿠폰 발급 로직
}
} finally {
lock.unlock();
}
정리
| 재고 | 차감포인트 | 차감쿠폰 | 발급 |
| 충돌 빈도 | 높음 | 낮음 | 매우 높음 |
| 멀티 서버 | 단일 자원 | 사용자별 독립 | 공유 자원 |
| 선택한 락 | 비관적 락 | 낙관적 락 | Redis 분산락 |
| 선택 이유 | 정합성 100% 보장 | 오버헤드 최소화 | 서버 간 락 공유 |
'💻 PROJECT > Team Projects' 카테고리의 다른 글
| [ 🚚 ONESTOP ] CI/CD부터 관리자 API까지 - 기술 선택의 이유 (0) | 2026.05.18 |
|---|---|
| [ 🚚 ONESTOP ] 정책 설계 — 옵션 중복 방지와 상태 관리 (0) | 2026.05.15 |
| [ 🚚 ONESTOP ] Docker Compose + GitHub Actions로 팀 개발 환경 & CI 구축하기 (0) | 2026.05.14 |
| [ 🚚 ONESTOP ] Spring Boot 프로젝트 기술 선택 이유 정리 (0) | 2026.05.13 |
| [ 😺 GitHub ] Git 활용하여 팀 소개 페이지 만들기 (1) | 2026.01.07 |