[DCommerce] 4편 - Redis로 재고 동시성 문제 해결하기

Do Hyun ·2026년 5월 7일

commerce-project

목록 보기
4/6

[DCommerce] 4편 - Redis로 재고 동시성 문제 해결하기

왜 동시성 문제가 발생할까?

기존 코드는 이런 흐름이었다:

1. DB에서 재고 조회
2. 재고 확인
3. 재고 차감
4. DB 저장

동시에 2명이 주문하면:

재고: 1개

A: 재고 조회 → 1개 있음
B: 재고 조회 → 1개 있음 (A가 아직 차감 안 함)
A: 재고 차감 → 0개
B: 재고 차감 → -1개 💥 overselling 발생!

재고가 마이너스가 되는 overselling 문제가 발생한다.


Redis로 해결하는 이유

Redis의 increment 연산은 원자적(Atomic) 으로 처리된다.

동시에 100명이 요청해도 Redis는 순서대로 하나씩 처리
→ 재고가 절대 음수가 되지 않음

Redis 설정

의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

application.yml

spring:
  data:
    redis:
      host: localhost
      port: 6379

RedisConfig

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, String> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }
}

StockService 구현

Redis에 재고를 관리하는 서비스.
키 형식은 stock:{productId} 로 상품별로 관리한다.

@Service
@RequiredArgsConstructor
public class StockService {

    private final RedisTemplate<String, String> redisTemplate;

    private String getStockKey(Long productId) {
        return "stock:" + productId;
    }

    // 재고 초기화 (상품 등록 시 호출)
    public void initStock(Long productId, int quantity) {
        redisTemplate.opsForValue().set(getStockKey(productId), String.valueOf(quantity));
    }

    // 재고 차감 (원자적 연산 핵심!)
    public Long decreaseStock(Long productId, int quantity) {
        return redisTemplate.opsForValue().increment(getStockKey(productId), -quantity);
    }

    // 재고 증가 (차감 실패 시 롤백용)
    public void increaseStock(Long productId, int quantity) {
        redisTemplate.opsForValue().increment(getStockKey(productId), quantity);
    }

    // 재고 조회
    public int getStock(Long productId) {
        String value = redisTemplate.opsForValue().get(getStockKey(productId));
        return value == null ? 0 : Integer.parseInt(value);
    }
}

상품 등록 시 Redis 재고 초기화

public void createProduct(ProductCreateRequestDTO dto) {
    Product savedProduct = productRepository.save(product);
    stockService.initStock(savedProduct.getId(), savedProduct.getStockQuantity());
}

주문 시 Redis 재고 차감

@Transactional
public void createOrder(Long memberId, CreateOrderRequestDTO dto) {
    // Redis에서 원자적으로 재고 차감
    Long remainStock = stockService.decreaseStock(
            dto.getProductId(),
            dto.getQuantity()
    );

    // 재고 부족 시 롤백
    if (remainStock < 0) {
        stockService.increaseStock(dto.getProductId(), dto.getQuantity());
        throw new IllegalArgumentException("재고가 부족합니다.");
    }
}

왜 차감 후 체크하나?
Redis increment는 차감 후 결과를 반환한다.
음수가 되면 재고 초과이므로 즉시 롤백하고 예외를 던진다.


동시성 테스트

재고 7개 상품에 10개 동시 요청:

for i in {1..10}; do
  curl -s -X POST http://localhost:8080/order \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"productId": 1, "quantity": 1}' &
done
wait

redis-cli GET stock:1

결과

시작 재고: 7개
동시 요청: 10개
결과 재고: 0개 ✅
성공: 7개 / 실패(재고 부족): 3개 ✅

재고가 음수가 되지 않고 정확히 0에서 멈췄다.


정리 포인트

"Redis의 increment 연산은 싱글스레드 기반의 원자적 연산으로, 동시 요청이 몰려도 순서대로 처리되어 overselling을 방지합니다. 10개 동시 요청 테스트에서 재고 초과분은 정상적으로 차단됨을 확인했습니다."


오늘 배운 것

  • Redis Key-Value 구조와 활용법
  • increment 원자적 연산으로 동시성 문제 해결
  • overselling 문제와 해결 방법
  • 재고 차감 후 음수 체크 + 롤백 패턴
  • 동시성 테스트 방법 (curl 병렬 요청)

profile
우당탕탕

0개의 댓글