⭐ Main Project
실무 역량 강화 프로젝트
풀어낸 문제들
플래시 세일에서 반드시 마주치는 핵심 문제들과 해결 접근
조회 성능 향상
Redis 캐싱 전략트래픽 폭주를 대비하여 조회 경로를 Redis로 끊어내 DB 커넥션 고갈 방지
플래시 세일의 특성상 판매 시작 몇 초 동안 상품 목록·상세 조회 트래픽이 수십 배로 튀는 구간이 존재합니다. 이 구간에 DB를 그대로 노출하면 커넥션이 금방 고갈되기 때문에, 조회 경로를 아예 Redis로 끊어내는 방향으로 설계했습니다.
5초 주기 스케줄러가 DB에서 목록을 뽑아 products:list 키에 덮어씀. 판매 수량 랭킹은 별도로 계산하지 않고, 조회 시점에 stock:{id} 실시간 재고를 mget 배치로 가져와 initialStock - stock 으로 역산
정각/30분마다 실행되는 캐시 워밍 스케줄러가 60분 이내 시작 예정인 이벤트 상품을 미리 Redis에 적재. 판매 시작 시점에는 이미 따뜻해진 캐시에서 바로 응답
모든 캐시 키는 saleEndAt + 10분으로 TTL을 걸어 이벤트 종료 후 자동 소멸. 이벤트가 없을 땐 캐시 자체를 저장하지 않아 불필요한 메모리 점유를 막음
캐시 미스와 장애(Redis 자체 오류)를 의도적으로 구분하여 에러 코드를 분리했습니다. 상세 페이지에서 정적 데이터 캐시가 없으면 "아직 워밍 전"으로 보고 PRODUCT_CACHE_NOT_FOUND, Redis 연결이 끊기면 REDIS_ERROR로 전역 핸들러에서 처리하도록 두 레이어로 분리한 것이 의도적인 설계 지점입니다.
재고 동시성 제어
Redis DECR + Fail-FastRedis의 원자 연산으로 경합을 해소하여 선착순 초과 판매 차단
선착순에서 가장 골치 아픈 게 초과 판매입니다. 일반적인 DB 비관적 락으로 막으면 경쟁이 심할수록 커넥션이 묶여 응답이 뭉쳐버립니다. Redis의 원자 연산(DECRBY) 으로 DB 진입 전에 재고를 먼저 선점하고, 음수가 나오면 DB까지 가지 않고 즉시 거절(Fail-Fast)하도록 처리했습니다.
재고는 DB에 컬럼을 두지 않고 stock:{id}에서만 관리합니다. DB products 테이블에는 initial_stock (불변)만 두고, 실제 판매 수량은 orders 레코드로 역산합니다. "재고의 단일 진실 원천"을 Redis로 못 박으면서 동기화 이슈를 애초에 없앤 설계입니다.
CountDownLatch로 200개 스레드를 같은 순간 출발시켜 재고 100개 상품을 두고 경쟁시키는 테스트로 확인했고, 성공 100건 / 실패 100건 / DB 주문 100건 / Redis 잔여 0개가 정확히 맞아떨어지는 것을 회귀 테스트로 고정했습니다.
장애 격리
Transactional Outbox + Redis Streams주문 서버와 포인트 서버 간 장애 전파 차단
적립 로직을 주문 트랜잭션 안에 두면, 포인트 서버가 느려지는 순간 주문 응답 시간도 같이 늘어납니다. 반대로 완전히 비동기로 던지면 메시지가 유실될 위험이 있어, Transactional Outbox 패턴으로 DB에 "보낼 메시지"를 먼저 기록하고 → 스케줄러가 Redis Streams로 발행하는 구조를 택했습니다.
중복 메시지가 유입될 수 있는 포인트(XADD 성공 후 DONE 업데이트 실패 시 재발행)는 points.order_id에 UNIQUE 제약을 걸고, 애플리케이션 레벨에서도 existsByOrder_Id로 한 번 더 확인하는 이중 방어로 막았습니다.
소비자 쪽에서는 실패 유형에 따라 XACK 전략을 분기했습니다. DB 저장 같은 일시적 실패는 XACK하지 않고 PEL(Pending Entry List)에 남겨 30초 주기로 재처리, 반대로 "주문이 존재하지 않음" 같은 복구 불가 에러는 무한 재시도를 막기 위해 XACK + 에러 로그로 종결시켰습니다.
Consumer Group이 사라졌을 때(스트림 키 TTL 만료 후 재생성 등)의 NOGROUP 예외도 메시지 체인까지 파고들어 잡아내서 그룹을 재생성하도록 처리했습니다.
설계하면서 의도적으로 내린 결정들
동기화 포인트를 없애기 위해 Redis를 단일 진실 원천으로 고정
fallback을 만들면 사실상 별도 시스템이 되고, 플래시 세일에서는 "재고 불일치보다 주문 중단이 안전"하다는 판단
stream:point:maxlen 키에 총 재고 × 2를 넣어 두고 발행 스케줄러가 이 값으로 trim하도록 위임 — 스트림이 무한정 커지는 걸 이벤트 단위로 자동 제어