허진혁Backend Developer
전체 프로젝트

2023.09 ~ 2024.01
Backend

Givemeticon

만료가 임박한 기프티콘을 사고파는 중고거래 플랫폼

선착순 쿠폰 발급 구조를 측정 결과에 따라 재설계하고, 인기순 조회의 성능을 개선했습니다.

Java · Spring Boot · MyBatis · MySQL · Redis · RabbitMQGitHub

01 / SCALABILITY · RabbitMQ, MySQL, Redis

측정 결과를 따라 세 번 바꾼 선착순 쿠폰 발급 구조

문제1,000명이 동시에 신청하는 선착순 쿠폰에서 초과 발급, 처리량 한계, DB 장애 시 신청 유실이 드러났습니다.

선착순 쿠폰 발급 구조의 변화 · 판정과 기록의 위치
BEFORE 응답 전에 MySQL이 재고 행을 잠그고 판정
신청
MySQL재고 행 잠금 → 차감 → 기록
응답

잠금 없이는 107장 발급, 잠그면 656rps에서 정체 · DB가 멈추면 신청도 멈춤

AFTER RabbitMQ 접수 + MySQL 단일 판정
신청
RabbitMQ저장 확인
202 접수 번호
응답 뒤 · 소비자 1개가 순서대로
MySQL 트랜잭션중복 확인 → 재고 차감 → 결과 기록
커밋 후 ACK

UNIQUE · 조건부 차감 · 대가: 결과 확인까지 지연

실제 트래픽이 없어 부하·장애 실험 환경을 만들고,
구조의 한계가 측정으로 드러날 때마다 다음 구조를 골랐습니다.

  • ① 처리량 · MySQL 판정 → Redis 판정: 재고 행에 비관적 락을 걸어 초과 발급은 0이 됐지만 처리량이 656rps에서 멈췄습니다. 행 잠금·커넥션 풀·fsync를 하나씩 바꿔 800rps에서 재측정했고, 재고 행을 4개로 나누자 p50이 3,362ms에서 2.4ms가 되어 원인을 한 행의 잠금 대기로 특정했습니다. 판정을 Redis Lua 원자 카운터로 옮겨 응답이 밀리지 않는 최대 부하가 400에서 1,600rps가 됐습니다.
  • ② 가용성 · Redis 판정 → Redis 판정 + RabbitMQ: DB를 약 48초 멈추자 성공 응답이 54초간 0이었고 당첨 기록 200건이 유실됐습니다. 응답 경로에서 DB를 빼고 브로커가 저장을 확인한 뒤 응답하도록 바꿔, DB 중단 중에도 신청 31,002건이 전부 성공했습니다.
  • ③ 정합성 · Redis 제거: 판정은 Redis, 기록은 MySQL에 있어 둘이 어긋나면 정답을 정할 기준이 없었습니다. 대조 실험에서 재시작 후 5,013장이 초과 발급됐고, Redis를 빼 판정과 기록을 MySQL 트랜잭션 하나에 두었습니다.
Outbox · Redis Streams · Kafka · RabbitMQ를 비교한 이유
대안판단
Outbox · DB 워커신청을 먼저 DB에 써야 해서 응답 경로에서 DB를 빼지 못합니다.
Redis Streams기록이 카운터와 같은 Redis에 남아 장애가 분리되지 않습니다.
Kafka가능하지만 메시지 하나가 실패했을 때 그것만 다시 받으려면 재시도 경로를 따로 설계해야 합니다.
RabbitMQ · 선택저장 후 확인, 메시지별 확인·재전달이 가능합니다.
  • 접수: 브로커 저장 확인을 받은 뒤에만 202와 접수 번호를 응답합니다. 확인이 늦으면 접수 번호와 함께 503을 주고, 큐가 가득 차면 새 신청을 거절합니다.
  • 판정: 소비자 1개가 순서대로 MySQL 트랜잭션 하나에서 중복 확인 → 재고 차감 → 결과 기록을 처리합니다. 같은 접수 번호는 한 번만, 한 사람은 한 장만(UNIQUE), 재고가 남을 때만 차감합니다.
  • 확인: DB에 커밋한 뒤에 처리 완료(ACK)를 알려, 소비자가 죽어도 판정이 사라지지 않게 했습니다.
  • 장애 처리: DB 연결이 끊긴 오류면 소비를 멈췄다 재개하고, 그 밖의 실패는 3번 시도 후 격리 큐로 보냅니다.
800rps에서이전변경
신청 응답 p9535~121ms18~92ms
당첨 결과를 아는 시점응답 즉시중앙값 42~53초 뒤
소비 속도(행/초)740~890320~440
신청당 DB 기록1행2행

응답과 확정 시점은 두 구조 각 3회, 소비 속도는 큐가 줄어든 기울기로 구한 값입니다. 판정 위치와 기록량이 함께 바뀌어 차이를 하나의 원인으로 돌리지 않습니다.

40회정합성 테스트 · 매회 재고 100장, 초과 발급 0
31,001건DB 중단 중 신청 전부 접수 · 복구 후 유실 0
620건 → 0건커밋 후 ACK 전 강제 종료, 재전달 중 중복 저장

1,000명 동시 신청·재고 100장을 10회 단위로 반복하고, 부하 중 DB·브로커를 멈추거나 소비자를 강제로 종료했습니다. 부하는 k6로 초당 요청 수를 단계별로 올렸고, 측정 전에 예상 결과를 문서에 적은 뒤 맞았는지 판정했습니다.