삭제된 게시글 반복 조회 문제 해결
문제
- 인기글 삭제 뒤에도 검색 결과와 공유 링크, 방문 기록에 기존 URL이 남아 같은 게시글 ID 조회 반복
- Redis에 조회 결과가 없는 것인지 실제로 게시글이 없는 것인지 구분하지 못하므로 요청마다 Article Service와 MySQL 재조회
해결 전략
- Article Service에서 404를 받은 게시글 ID는 Redis에
MISSING값으로 저장하고 TTL 60초 적용 - 부재 캐시가 있으면 원본을 조회하지 않고 즉시 404 반환
- 같은 ID의 동시 요청은 한 건만 원본을 조회하고 나머지는 캐시 생성까지 대기
기술 선택 이유
-
Bloom Filter - 제외
- 게시글 생성과 삭제 때마다 동기화와 재구축 필요. 단일 ID 집중 문제에 비해 운영 부담이 큼
-
짧은 부재 캐시 - 채택
- 게시글 생성과 삭제 때 별도 동기화가 필요 없음
- 서비스에서 이미 사용하고 있는 Redis에 키 하나만 추가해 적용
구현
// 부재 캐시가 있으면 원본을 조회하지 않고 즉시 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초 |
| 비교 | 변경 전, 부재 캐시 |
| 지표 | 변경 전 | 부재 캐시 |
|---|---|---|
| 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건확인
한계
- 락을 얻지 못한 요청은 2초까지만 대기해 원본 조회가 그보다 길어지면 대기 중인 요청이 한꺼번에 503으로 실패
- 존재하지 않는 ID가 서로 다른 값으로 대량 유입되면 ID마다 원본을 한 번씩 조회. 검증은 삭제된 인기글 ID 1개로 단일 ID 집중만 측정