DEVELOPMENT NOTE / Modu Square
삭제된 게시글 반복 조회 문제 해결
문제
- 인기글 삭제 뒤에도 검색 결과·공유 링크·방문 기록에 기존 URL이 남아 같은 게시글 ID 조회 반복
- Redis에 조회 결과가 없는지 실제로 게시글이 없는지 구분하지 못하므로 요청마다 Article Service와 MySQL 재조회
기술 선택 이유
-
Bloom Filter — 제외
- 게시글 생성·삭제 때마다 동기화와 재구축 필요. 단일 ID 집중 문제에 비해 운영 부담이 큼
-
짧은 부재 캐시 — 채택
- 원본에서 404가 확인된 ID에
MISSING값을 60~70초 저장 - 타임아웃과 5xx는 캐시하지 않아 원본 장애를 게시글 부재로 오인하지 않음
- 조회 모델과 부재 캐시가 모두 없을 때만 Redis
SET NX로 3초 잠금 획득 - 잠금을 얻은 요청 한 건만 원본을 확인하고 나머지는 결과 생성까지 대기
- 원본에서 404가 확인된 ID에
구현
// 부재 캐시가 있으면 원본을 조회하지 않고 즉시 404 반환
if (missingArticleCacheRepository.isMissing(articleId)) {
throw articleNotFound(articleId);
}
// 동일 ID의 동시 원본 조회는 한 요청만 수행
String lockOwner = UUID.randomUUID().toString();
if (!articleLookupLockRepository.tryAcquire(articleId, lockOwner)) {
// 잠금을 얻지 못한 요청은 캐시가 생성될 때까지 대기
return awaitLookupResult(articleId);
}
try {
// 조회 모델과 부재 캐시가 모두 없으므로 원본 서비스에서 게시글 조회
Optional<ArticleQueryModel> loaded = fetch(articleId);
if (loaded.isEmpty()) {
// 확인된 404를 짧게 저장해 후속 원본 조회 차단
missingArticleCacheRepository.markMissing(articleId);
}
return loaded;
} finally {
// 예외가 발생해도 자신이 획득한 잠금 해제
articleLookupLockRepository.release(articleId, lockOwner);
}
- 잠금 획득 후 조회 모델과 부재 캐시를 다시 확인해 중복 원본 조회 방지
- 잠금을 얻지 못한 요청은 최대 2초 동안 조회 결과나
MISSING생성 대기
부하 테스트
| 항목 | 설정 |
|---|---|
| 대상 | 삭제된 인기글 ID 1개 |
| 부하 | 2,000 req/s · 30초 |
| 방식 | k6 constant-arrival-rate |
| 비교 | 변경 전 · 부재 캐시 |
검증 결과
| 지표 | 변경 전 | 부재 캐시 |
|---|---|---|
| Article Service 원본 조회 | 55,688건 | 1건 |
| p95 | 859.05ms | 1.26ms |
| 시작하지 못한 요청 | 4,313건 | 0건 |

- p95
859.05ms → 1.26ms, p99953.09ms → 6.24ms로 감소

- 시작하지 못한 요청
4,313건 → 0건, 원본 조회55,688건 → 1건확인