pile·
백엔드·마켓컬리마켓컬리 Hello World·

풀필먼트 입고 서비스팀에서 Spring Redisson으로 분산락 사용하기

컬리 풀필먼트 입고서비스(RMS)에서 카프카 중복 발주·더블클릭·동시 재고 트랜잭션 같은 동시성 문제를 해결하기 위해, Redisson 기반 분산락을 어노테이션(AOP)으로 손쉽게 쓰도록 만든 방법을 다룬다. 락 처리 로직을 비즈니스 로직과 분리하고, 트랜잭션 커밋 이후 락을 해제해 데이터 정합성을 보장하는 게 핵심이다.

핵심 포인트
  • 멀티 인스턴스 환경에서 공통 락이 필요해 Redis와 Redisson을 채택했다: 팀이 이미 Redis를 써 추가 인프라가 불필요했다.
  • Redisson은 Lettuce와 달리 Lock interface(타임아웃 등)를 제공하고, 스핀락 대신 Pub/Sub 방식이라 Redis 부하가 작다.
  • @DistributedLock 어노테이션과 AOP로 waitTime·leaseTime·SpEL 기반 동적 락 이름을 커스텀했다.
  • 락 메서드를 Propagation.REQUIRES_NEW로 별도 트랜잭션 실행하고, 반드시 커밋 이후 락을 해제한다.
  • 쿠폰 100장 동시 100요청·중복 발주 10건 테스트로 락 유무에 따른 정합성 차이를 검증했다.
상세 정리
  • 동시성 이슈: 카프카 중복 발주 수신, 검수/검품 더블클릭 중복 요청, 이동 출고 시 다중 작업자 CTA로 잘못된 재고 트랜잭션 등으로, 애플리케이션 예외 처리만으론 부족했다.
  • 라이브러리 선택: 분산락은 Redis·MySQL·Zookeeper로 가능하나, MySQL은 별도 커넥션 풀 관리와 RDS 부하가 부담이라 Redis가 효율적이었다.
  • Redisson vs Lettuce: Lettuce는 setnx/setex로 직접 구현하고 retry·timeout을 수동 처리하며 스핀락이라 요청이 많을수록 Redis 부하가 커진다. Redisson은 Lock interface와 Pub/Sub(해제 신호 수신 후 획득)로 안전하고 부하가 작다.
  • 컴포넌트 설계 규칙: 락 로직을 비즈니스와 분리, waitTime·leaseTime 커스텀, 락 이름 사용자 지정, 공통 요구사항 일괄 관리를 규칙으로 삼았다.
  • 구현: DistributedLockAop의 @Around에서 SpEL로 락 key를 파싱(#lockName, #model.getName() 등 동적 조합)해 RLock을 획득하고, tryLock(waitTime, leaseTime) 후 AopForTransaction.proceed로 대상 메서드를 실행하며, finally에서 무조건 unlock한다. 이미 해제된 경우 IllegalMonitorStateException을 로깅한다.
  • 트랜잭션 순서: 락 해제가 커밋보다 빠르면 재고 10 조회 후 차감(미커밋) 상태에서 락을 풀어, 다른 클라이언트가 여전히 10으로 조회해 중복 차감하면 정합성이 깨진다. 커밋 후 해제하면 후속 클라이언트가 갱신된 재고를 조회해 정확히 차감한다.
  • 검증 1: 쿠폰 100장에 100스레드 동시 요청 시 분산락 적용은 정확히 100장 차감(잔여 0), 미적용은 79장만 차감돼 정합성이 붕괴했다.
  • 검증 2: 동일 코드 발주 10건 동시 수신 시 락 적용은 1건만 등록됐고, 미적용은 validation을 통과해 다수 등록됐다. 등록 개수는 커넥션 풀 크기에 따라 변동한다.
  • 효과: 락 처리를 핵심 로직과 분리해 가독성·생산성이 오르고, 안전한 동시성 제어를 어노테이션으로 재사용할 수 있게 됐다.
왜 읽나Spring에서 멀티 인스턴스 동시성을 다루는 백엔드 개발자에게 Redisson AOP 분산락 구현과 커밋 후 해제 원칙을 코드로 보여주는 실전 예제.
마켓컬리
마켓컬리 Hello World 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·github-engGitHub Engineering·

    조기 종료를 없애야 벡터화된다 — 메모리 속도 소스 코드 케이스 폴딩

    GitHub의 코드 검색 엔진 Blackbird는 480TB 이상의 소스 코드를 인덱싱하기 전 모든 바이트에 case folding을 적용한다. 이 글은 Rust로 구현한 case folding을 메모리 대역폭 한계(45+ GiB/s)까지 끌어올린 두 가지 반직관적 최적화를 상세히 다룬다. 핵심은 루프 조기 종료(break) 제거로 LLVM 벡터화를 유도하고, UTF-8을 디코딩하지 않고 바이트 공간 산술만으로 fold를 수행하는 것이다.

    #rust#unicode#simd+2
  2. 백엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    트랜잭션 스크립트에서 숙소 메타 + 가격 계산 모듈로 — 전시 아키텍처 개선기 (2/3)

    여기어때 전시개발팀이 숙소 상세(PDP) API를 해부한 결과, 코드상으로는 DB 호출 3번처럼 보이던 요청이 실제로는 MongoDB $lookup 체인으로 컬렉션을 19회 접근하는 구조였다. 이 트랜잭션 스크립트 방식의 핵심 문제는 "aggregation이 I/O를 가린다"는 점으로, 독립적인 쿼리 10개가 단일 파이프라인에 직렬화되어 병렬화 기회를 잃고, 가격 때문에 거의 안 바뀌는 이미지까지 매 요청마다 읽어야 하는 읽기 증폭이 발생했다. V3에서는 "조회 시점 조립"을 "쓰기 시점 사전 조립"으로 전환하고, 화면별로 복제되던 가격 계산 로직을 goodsprice 단일 모듈로 수렴했다. 4개 API(PLP/PDP/RDP/ILP)의 반복 마이그레이션은 Claude Code skill로 절차를 고정하고 쉐도잉 + 동일성 검증으로 안전망을 마련하는 방식으로 진행됐다.

    #architecture#migration#caching+2