이번 주의 핵심 목표는 Redis의 자료구조를 활용해 DB 부하를 줄이고, 실시간 처리 및 비동기 처리를 안정적으로 구성할 수 있는 구조를 설계하고 구현하는 것이었다.
이를 위해 다음 두 가지 기능을 중심으로 설계를 진행했다:
count < total 조건을 통과한 요청이 중복 발급되는 현상 발생 가능ZADD product:ranking:{date} {score} {productId}
EXPIRE product:ranking:{date} 7d
RecordProductSalesEvent를 @TransactionalEventListener로 처리하여 주문과 분리RankingStrategy를 도입해 OCP 확장public interface RankingStrategy {
String key(ProductRankingCommand command);
LocalDate date();
}
1. Publisher: Redis Stream 에 발급 요청 저장
2. Consumer: ConsumerGroup 으로 메시지 처리
→ 중복 검사, DB 저장, ACK
→ 실패 시 DLQ로 전송
3. DLQConsumer: 실패 메시지 재시도, 3회 이상 실패 시 폐기
retry:{recordId} Redis 키로 재시도 횟수 관리REQUIRES_NEW 트랜잭션을 사용하여 처리 단위를 작게 분리Awaitility로 Redis Stream 메시지 처리 대기| 구분 | 기술 | 목적 | 설계 포인트 |
|---|---|---|---|
| 랭킹 기록 | Redis Sorted Set | DB 부하 제거 | TTL 설정, 전략 패턴 |
| 이벤트 처리 | @TransactionalEventListener | 트랜잭션 분리 | 주문 → 통계 비동기화 |
| 발급 큐 | Redis Stream | 대량 트래픽 흡수 | Consumer Group 처리 |
| 실패 복구 | DLQ + Retry | 메시지 유실 방지 | 최대 3회 재시도 |
이번 주는 단순 기능 구현을 넘어서, "운영 가능한 구조란 무엇인가"에 대해 깊이 고민한 시간이었다.
Redis는 캐시 도구가 아니라 하나의 구조적 저장소이며 처리 흐름의 중심이 될 수 있다는 가능성을 실감했다. 특히 Redis Stream은 Kafka가 없더라도 실시간 트래픽 처리, 재시도, 실패 보완까지 충분히 커버 가능하다는 점에서 강력한 무기가 될 수 있음을 확인했다.
비동기 시스템은 단순히 큐에 메시지를 밀어넣는 게 아니라, 실패했을 때의 복구 전략까지 함께 설계되어야 진정한 운영 가능한 구조가 된다는 사실도 체감했다. DLQ, retry count, REQUIRES_NEW 트랜잭션 분리 같은 것들이 그 고민의 결과였다.
또한 랭킹 시스템 구현에서 OCP를 만족하는 전략 패턴을 도입한 경험은 단지 정렬 기준의 유연성 확보가 아니라, 구조적 설계가 시스템의 유지보수성과 직결된다는 것을 보여주는 좋은 사례였다.
이번 주는 Redis라는 외부 인프라를 신뢰할 수 있는 일관성 보장 시스템으로 끌어올리는 실전 연습이었다. 코드뿐 아니라 아키텍처, 장애 상황, 테스트 전략까지 종합적으로 고민하고 구현한 한 주였다.