항해 백엔드 7주차 회고(WIL): 랭킹과 쿠폰, 실시간 시스템

JUNYOUNG·2025년 5월 16일

항해 플러스 백엔드

목록 보기
12/14
post-thumbnail

이번 주 목표

이번 주의 핵심 목표는 Redis의 자료구조를 활용해 DB 부하를 줄이고, 실시간 처리 및 비동기 처리를 안정적으로 구성할 수 있는 구조를 설계하고 구현하는 것이었다.

이를 위해 다음 두 가지 기능을 중심으로 설계를 진행했다:

  • Redis Sorted Set 기반의 실시간 인기 상품 랭킹 시스템
  • Redis Stream 기반의 비동기 쿠폰 발급 시스템 (DLQ + 재시도 포함)

문제 정의

1. 실시간 통계를 RDB에 저장하면 발생하는 성능 문제

  • 주문 확정 시마다 DB에 인기 상품 통계를 쓰는 구조는 RDB 쓰기 부하 집중락 충돌 발생 우려
  • 특히 주문이 폭주하는 피크 타임에는 통계 기록만으로도 DB 병목이 생길 수 있음

2. 선착순 쿠폰 발급에서의 Race Condition

  • 동시에 수천 명이 요청을 보내면, count < total 조건을 통과한 요청이 중복 발급되는 현상 발생 가능
  • 단순한 동기 락으로는 병렬성 확보가 어렵고, 무조건 실패시키는 방식은 사용자 경험을 해침

해결 전략 및 설계 방식


STEP13 – 실시간 랭킹 시스템 설계 (Redis Sorted Set)

설계 목표

  • 실시간 인기 상품 통계 기록을 RDB가 아닌 Redis로 우선 처리
  • 기간별(일간/주간/월간) 랭킹 구분과 향후 확장성 고려

구조 구성

ZADD product:ranking:{date} {score} {productId}
EXPIRE product:ranking:{date} 7d
  • Sorted Set을 활용하여 주문 수를 점수로 누적
  • TTL을 이용해 Redis 용량 관리 + 최신성 유지
  • RecordProductSalesEvent@TransactionalEventListener로 처리하여 주문과 분리

전략 패턴 적용

  • 기간별 랭킹 생성을 위해 RankingStrategy를 도입해 OCP 확장
  • 일간/주간/월간 기준에 따라 Redis Key/날짜 생성 방식만 변경되도록 추상화
public interface RankingStrategy {
    String key(ProductRankingCommand command);
    LocalDate date();
}

STEP14 – 비동기 쿠폰 발급 (Redis Stream + DLQ)

설계 목표

  • 트래픽이 몰려도 발급 요청을 안전하게 수용하고, 실패 시 복구 가능한 구조로 설계
  • DB가 아닌 Redis Stream으로 요청을 버퍼링 → Consumer가 순차 처리

전체 처리 흐름

1. Publisher: Redis Stream 에 발급 요청 저장
2. Consumer: ConsumerGroup 으로 메시지 처리
   → 중복 검사, DB 저장, ACK
   → 실패 시 DLQ로 전송
3. DLQConsumer: 실패 메시지 재시도, 3회 이상 실패 시 폐기

재시도/보상 설계

  • DLQ 메시지는 retry:{recordId} Redis 키로 재시도 횟수 관리
  • REQUIRES_NEW 트랜잭션을 사용하여 처리 단위를 작게 분리
  • 중복 발급 방지는 Redis + DB에서 이중 체크 → 완벽한 멱등성 확보

테스트 전략

  • Embedded Redis 기반 통합 테스트에서 Awaitility로 Redis Stream 메시지 처리 대기
  • Consumer 수동 호출 → 성공/실패/재시도까지 End-to-End 시나리오 검증
  • 발급 실패 시 DLQ → 재처리 후 DB 저장 여부까지 체크

인사이트

  • Redis는 단순 캐시뿐 아니라 실시간 처리 버퍼, 순위 정렬, 중복 제어 등 다양한 역할로 활용 가능
  • 비동기 설계에서 중요한 건 실패 복구 시나리오를 구조적으로 설계하는 것
  • 테스트는 "정상 작동"만이 아니라 실패 → 복구 → 재시도 흐름까지 고려해야 설계의 완성도가 높아짐
  • Kafka 없이도 Redis Stream만으로 충분히 트래픽 흡수 + 처리를 분리하는 설계 가능

요약 정리

구분기술목적설계 포인트
랭킹 기록Redis Sorted SetDB 부하 제거TTL 설정, 전략 패턴
이벤트 처리@TransactionalEventListener트랜잭션 분리주문 → 통계 비동기화
발급 큐Redis Stream대량 트래픽 흡수Consumer Group 처리
실패 복구DLQ + Retry메시지 유실 방지최대 3회 재시도

총평

이번 주는 단순 기능 구현을 넘어서, "운영 가능한 구조란 무엇인가"에 대해 깊이 고민한 시간이었다.

Redis는 캐시 도구가 아니라 하나의 구조적 저장소이며 처리 흐름의 중심이 될 수 있다는 가능성을 실감했다. 특히 Redis Stream은 Kafka가 없더라도 실시간 트래픽 처리, 재시도, 실패 보완까지 충분히 커버 가능하다는 점에서 강력한 무기가 될 수 있음을 확인했다.

비동기 시스템은 단순히 큐에 메시지를 밀어넣는 게 아니라, 실패했을 때의 복구 전략까지 함께 설계되어야 진정한 운영 가능한 구조가 된다는 사실도 체감했다. DLQ, retry count, REQUIRES_NEW 트랜잭션 분리 같은 것들이 그 고민의 결과였다.

또한 랭킹 시스템 구현에서 OCP를 만족하는 전략 패턴을 도입한 경험은 단지 정렬 기준의 유연성 확보가 아니라, 구조적 설계가 시스템의 유지보수성과 직결된다는 것을 보여주는 좋은 사례였다.

이번 주는 Redis라는 외부 인프라를 신뢰할 수 있는 일관성 보장 시스템으로 끌어올리는 실전 연습이었다. 코드뿐 아니라 아키텍처, 장애 상황, 테스트 전략까지 종합적으로 고민하고 구현한 한 주였다.

profile
Onward, Always Upward - 기록은 성장의 증거

0개의 댓글