
대용량 트래픽 & 데이터 처리
이번 주차는 이미 적용된 Redis의 자료 구조를 이해하고, 이를 활용해 실시간 랭킹, 선착순 쿠폰 발급과 같은 기능을 어떻게 설계할 수 있는 지 학습하는 과정이었습니다.
Redis 기반 시스템 설계 정리
이번 학습에서는 Redis 자료구조를 이해하고, 이를 활용해 실시간 랭킹, 대기열, 선착순 쿠폰 발급과 같은 기능을 어떻게 설계할 수 있는지 다루었습니다.
Redis 자료구조
Redis는 단순한 Key-Value 저장소가 아니라 다양한 컬렉션 형태를 지원하는 고성능 데이터 저장소입니다. 이를 통해 단순 캐시뿐만 아니라 실시간 데이터 처리에 활용할 수 있습니다.
Strings
- 일반 문자열 데이터를 저장하며, 단순 증감 연산도 지원합니다.
- 예:
INCR, SET, GET
Sets
- 중복 없는 데이터를 저장하며, 교집합·합집합·차집합 연산이 가능합니다.
- 예:
SADD, SMEMBERS, SINTER
Sorted Sets
- Set에
score라는 가중치를 추가하여 정렬을 지원하는 구조입니다.
- 점수 기반으로 순위 계산이 가능하며, 랭킹 시스템에 적합합니다.
- 예:
ZADD, ZREVRANK, ZINCRBY
Redis 기반 랭킹 시스템 설계
랭킹 시스템은 Sorted Set 자료구조를 통해 효율적으로 구현할 수 있습니다.
ZADD key score member : 점수와 함께 데이터 저장
ZINCRBY : 특정 멤버의 점수 증가
ZREVRANGE : Top N 사용자 조회
ZREVRANK : 특정 사용자의 순위 조회
응용 예시
-
일간/주간 랭킹 분리:
- 키 예시 →
ranking:daily:20240824, ranking:weekly:2024-W34
- TTL을 적용하여 기간이 끝나면 자동 만료
Redis 기반 쿠폰 발급
대기열
- 순서가 중요한 쿠폰 발급의 대기열은 List 또는 Sorted Set으로 구현할 수 있습니다.
LPUSH, RPOP을 이용해 FIFO 방식으로 처리합니다.
선착순 쿠폰 발급
- 쿠폰 재고는 List, 발급자 기록은 Set으로 관리합니다.
SADD를 이용해 중복 발급을 방지하고, LPOP으로 쿠폰 수량을 제어합니다.
설계 시 주의사항
- TTL을 반드시 설정하여 불필요한 데이터가 계속 쌓이지 않도록 합니다.
- DB와 Redis를 함께 사용할 때는 데이터 정합성 보장이 필요합니다.
- 키 네이밍 전략을 일관성 있게 가져가야 관리가 용이합니다.
학습 정리
- Redis는 단순 캐시를 넘어 다양한 실시간 기능 구현에 활용할 수 있습니다.
- Sorted Set을 통한 랭킹 시스템, Set과 List를 활용한 쿠폰 발급이 대표적인 사례입니다.
- TTL과 원자성을 적극적으로 활용하면 고성능·고가용성 시스템을 구현할 수 있습니다.
Redis 기반 상품 랭킹 설계
목표
- 기존 DB(NativeQuery) + 캐싱 기반의 상품 랭킹에서, Redis 기반 집계 방식으로 전환
- 트래픽 급증 상황에서도 빠른 랭킹 조회 성능 확보
- 조회 조건: 오늘을 포함한 최근 3일 간 랭킹
기존 방식
- DB 쿼리 기반 + 캐싱
- 사용자 조회 시 캐시 확인 → 없으면 DB Native Query 실행
- 실행 결과를 Redis 캐시에 저장 (자정 만료)
- 장점: DB 데이터 정합성 100% 보장
- 단점: 조회 시점마다 DB 부하 발생 → 트래픽 급증 시 성능 저하
개선 방식 (Redis 기반 집계)
- 주문 발생 시점에 Redis에 바로 집계 반영
- 주문 생성 시 Redis Sorted Set(
ZSET)에 ZINCRBY로 상품 판매량 업데이트
- 랭킹 조회 시점
- Redis Sorted Set(ZSET)에서 누적된 값 바로 활용
- 최근 3일 집계:
ZUNIONSTORE 또는 ZUNION 연산으로 3일치 키 병합
- 정합성 보장
- Redis 업데이트는 트랜잭션 커밋 이후에만 수행 ←
차주 추가 구현, 일단 기능 수행
이벤트 트리거는 AOP @PublishedOrder로 표준화 ← 적절하지 않음
AOP는 횡단 관심사(cross-cutting concern)를 모듈화 하는데 적합하며,
특정 기능을 타겟하는 AOP는 오히려 비즈니스 로직이 가려지고, 가독성이 떨어지게 됨.
@TransactionalEventListener(AFTER_COMMIT) 구간에서 ZINCRBY 호출
Redis 자료구조를 활용해 보는 것으로 구현
차주에 이벤트 발행 방식으로 변경
- 중복 집계 방지
- 멱등 키(
RANK:PROCESSED:{orderId})를 함께 발급함
Key & 자료구조
Redis 자료구조
- Sorted Set (ZSet): 상품 별 판매량을 score로 관리하여 정렬 및 집계에 용이함.
- Key:
RANKING:PRODUCT:20250817
- Value: member - productId, score - quantity(판매수량)
Redis 기반 선착순 쿠폰 발급 설계
문제 정의
- 기존에 조건부 업데이트 쿼리를 통해서 초과 발급은 방지하였으나, 선착순은 보장되지 않음
- 트래픽이 몰리는 상황에서 Redis를 활용한 분산환경 및 선착순 제어 로직 추가 필요
설계 목표
- 선착순 보장: 초과된 요청에 대하여 즉시 차단
- 비동기 확정: 쿠폰 엔티티를 생성하는 확정 로직은 큐/비동기 프로세스에서 수행
- 장애 격리: Redis 장애 시 우아한 폴백(graceful fallback) 설계 - 학습
Key & 자료구조
-
남은 수량 관리
- key:
COUPON:POLICY:{policyId}:REMAINING
- 자료구조:
String 에 최초 발급 수량 저장
DECR로 원자적 차감, 0 미만 시 마감 처리
TTL: CouponPolicy.endedAt 기준으로 expire 설정 → 이벤트 종료 시 자동 정리
-
선착순 관리
- key:
COUPON:POLICY:{policyId}:PENDING
- 자료구조:
ZSET(userId, timestamp)
- Redis 선착순 통과 직후 사용자를 넣음
- score: 요청 시간(ms + 랜덤숫자) 저장 → "누가 몇 번째로 도착했는지" 순서 관리 가능
ms까지 동일한 경우를 대비해서, ms + 랜덤 숫자를 지정해야할까? 어느정도 중복이 되나
- ms까지 동일한데, 1장 남았을 경우가 과연 많을까 고민하였지만, 설계는 중복되지 않는 것을 목표로 작성했습니다.
- consumer가 DB 반영 후 ISSUED로 이동.
TTL
CouponPolicy.endedAt 이후 자동 삭제(기본)
Worker가 주기적(10초 미만)으로 poll → DB 반영 완료 후 ISSUED로 이동
- (질문): PENDING에 있는 값도 쿠폰 정책 종료 일자로 만료를 지정하는 것과 워커 실행 주기보다만 길게 가져가는 것에 대해 고민했습니다.
- 워커에서 100~1000개씩 처리를 한다고 하더라도,
-
발급 확정
- key:
COUPON:POLICY:{policyId}:ISSUED
- 자료구조:
SET(userId)
- DB 반영 완료된 사용자
- 중복 요청 차단 및 발급 최종 이력 확인용
TTL: CouponPolicy.endedAt 기준으로 expire 설정
피드백
- getTop5InLast3Days 또한 지속적으로 호출되는데, 상위 상품이라는 개념이 시간적으로 100% 실시간으로 반환하기 보단 5분정도의 캐싱도 좋을 것 같다.
- RedisService 개념은 부적절하다. 비즈니스라기보단 DB와 같이 저수준 모듈로 관리되어야 한다.
마치며
- 이번 주차도 재미있게 고도화된 백엔드 구성을 하는 과정이었습니다.
- 아직 레이어드 아키텍처에서 각각의 레이어, DTO 같은 기본 개념에서 모자람을 느끼는 주차였습니다.
- 다음 주차에도 이 기능을 고도화해서 이벤트와 관련된 기능으로 만드는데, 기능의 구현이나 로직의 순서는 감을 잡은 것 같으나, 아직도 구조에 약한 부분이 마음에 걸리는 주차였습니다.