일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- querydls
- 프로그래머스
- springboot
- 아키텍처
- MSA
- JPA
- Github Actions
- 유효성 검사
- aop
- rabbitmq
- Kafka
- trouble shooting
- testcode
- Intellij
- JWT
- Java
- 객체지향원칙
- 테스트 코드
- Redis
- EC2
- CI/CD
- DevOps
- Til
- 멀티 모듈
- algorithm
- docker
- 어노테이션
- swagger
- spring boot
- AWS
- Today
- Total
개발노트
2025.03.22 RabbitMQ concurrency 설정과 비관적 락을 활용한 재고 감소 동시성 제어 본문
2025.03.22 RabbitMQ concurrency 설정과 비관적 락을 활용한 재고 감소 동시성 제어
ddong-kka 2025. 3. 25. 11:00개요
MSA 환경에서 재고 감소 로직을 RabbitMQ와 함께 구현할 때, 메시지 처리 순서가 보장되지 않은 문제가 발생할 수 있다.
RabbitMQ는 비동기로 메시지를 처리하며, 소비자(Consumer)가 여러 개일 경우 메시지가 동시에 여러 스레드에서 처리될 수 있다.
문제 1: 메시지 처리 속도 차이로 인한 순서 불일치
@RabbitListener(queues = "${stockMessage.queue.stock.request}")
public void handleStockDecrementRequest(StockDecrementMessage stockDecrementMessage) {
productEventService.decreaseStock(stockDecrementMessage);
}
handleStockDecrementRequest() 메서드는 StockDecrementMessage를 받아 decreaseStock()을 실행한다.
하지만 내부적으로 decreaseStock()이 실행되는 속도가 다를 수 있어, 성공/실패 메시지가 원래 메시지 순서와 다르게 전송될 가능성이 있다.
문제 2: 데이터 정합성 문제
여러 개의 주문이 동시에 들어오면, 서로 다른 스레드에서 동일한 Product의 재고를 조회하고 감소할 수 있다.
이때, 재고가 충분하다고 판단된 주문들이 동시에 감소를 수행하면 재고 초과 감소(Overselling) 문제가 발생
문제의 원인
RabbitMQ가 기본적으로 여러 개의 Consumer를 동시에 실행할 수 있도록 설계되어있기때문에 특정 메시지를 처리하는 동안 다음 메시지가 병렬로 실행될 수 있다.
현재 나의 상황을 예시로 들면 이런 구조이다. 메시지가 순차적으로 들어왔다고 가정한다
- orderId 101 - 성공
- orderId 102 - 재고 부족 (실패)
- orderId 103 - 성공
처리 속도가 다를 경우 RabbitMQ에서 아래 같이 순서가 어긋난 결과를 반환하게되는것이 문제의 핵심
- orderId 102 - (실패)
- orderId 101 - 성공
- orderId 103 - 성공
이렇게 결과가 반환되면 주문 시스템에서는 orderId 102 가 실패한 후에도 orderId 101이 성공했다는 메시지를 받고, 데이터 의 정합성이 깨질 위험이 존재
해결 방법
리스너에 concurrency 옵션 적용
Spring AMQP에서는 @RabbitListener의 concurrency 속성을 설정하면 동시에 실행될 소비자(Consumer) 수를 조절할 수 있다는 것을 찾았다.
concurrency 가 설정되지않으면 RabbitMQ는 최대한 많은 Consumer를 생성하여 병렬 처리를 수행하게된다.
하지만 concurrency = "1" 로 설정하면 하나의 Consumer만 실행되므로 메시지의 순서대로 처리된다.
@RabbitListener(queues = "${stockMessage.queue.stock.request}", concurrency = "1")
public void handleStockDecrementRequest(StockDecrementMessage stockDecrementMessage) {
productEventService.decreaseStock(stockDecrementMessage);
}
- 한 번에 하나의 메시지 처리됨
- 처리 순서가 보장된다(FIFO)
- 동일한 큐에서 실행 순서가 보장되므로 성공/실패 응답이 순서대로 반환
JPA 비관 락 적용
비관락을 사용해 재고 감소 처리 중 다른 트랜잭션이 같은 상품을 수정하지 못하도록 방지하게끔 했다.
@Override
@Lock(LockModeType.PESSIMISTIC_WRITE) // 비관적 락 적용
@Query("SELECT p FROM Product p WHERE p.id = :productId AND p.deletedAt IS NULL")
Optional<Product> findByIdWithLock(@Param("productId") UUID productId);
이렇게 설정하면 동일한 상품에 대한 호출이 실행되면 다른 트랜잭션이 해당 상품을 수정할 수 없다
트랜잭션이 끝날 때까지 다른 요청이 같은 상품의 재고를 수정하려고 하면 대기하게된다.
재고 초과 감소 문제를 방지할 수 있음
'TroubleShooting' 카테고리의 다른 글
25.04.13 Spring Redis hashOps.keys()로 패턴 조회 안 되는 이유와 SCAN을 사용한 해결법 (0) | 2025.04.13 |
---|---|
25.04.10 Jackson LocalDateTime 직렬화 오류 해결 (0) | 2025.04.09 |
25.03.21 사가 패턴 무한 재시도 방지 (0) | 2025.03.21 |
25.03.17 RabbitMQ message 직렬화 문제 해결 (0) | 2025.03.17 |
25.02.18 Spring Boot MVC 바인딩의 순서 (0) | 2025.02.18 |