급한 문제가 아닙니다. 지금 배포 데이터로는 실해가 없고(운영 record 162건), 기록이 많은
헤비 유저가 생겼을 때 드러나는 문제입니다. 명세된 캐시가 미구현이라는 사실과 실측 근거를
남겨 두는 것이 목적이고, 개선 여부와 시점은 AI 파트 판단에 맡깁니다.
무엇을 발견했나
FeedKeywordRepository.PROFILE_KEYWORDS_SQL(회원 관심 Keyword 집계)이 회원 본인의 기록 수에
정비례하고, 피드 요청마다 실행됩니다.
천만 건 벤치 볼륨(S15P11A705-283, postgres 1Gi 한도)에서 회원 규모별 실측:
| 회원 record 수 |
Profile 쿼리 |
| 697건 |
5.8 ms |
| 4,000건 |
14.5 ms |
| 20,000건 |
216.4 ms |
record 20,000건 회원의 피드 API가 197ms인데 그중 163ms가 이 쿼리입니다
(S15P11A705-303으로 최신 채널을 236ms → 2.3ms로 줄인 뒤 남은 몫).
FeedService.rank()가 커서 페이지마다 다시 도는 구조라(코드 주석에 명시됨) 스크롤할 때마다
반복됩니다.
계획·인덱스로는 못 고칩니다
enable_mergejoin = off로 Nested Loop를 강제해 보면 77ms로 보이지만 8.8만 블록(약 690MB)을
만지며 ai.context_keyword를 Parallel Seq Scan으로 두 번 훑습니다. 어느 계획을 골라도 회원의
맥락 26,666건 × 키워드 테이블 조인 비용은 남습니다.
즉 쿼리 튜닝 대상이 아니라 호출 빈도를 줄이는 문제로 보입니다.
해법이 이미 명세에 있습니다
docs/ai/spec/feed-profile-cache.md 첫 줄:
현재 코드가 없는 구현 예정 명세입니다.
| Cache |
Key |
TTL |
| 사용자 관심 Profile |
feed:profile:{memberId} |
6시간 |
| Collection 특징 |
feed:coll:{collectionId} |
1시간 |
TTL 6시간의 근거도 이 쿼리를 두고 쓴 문장으로 읽힙니다 — "Record 하나가 추가돼도 분포가 크게
흔들리지 않으므로 길게 잡습니다. 계산 비용도 상대적으로 큽니다."
Redis는 이미 떠 있고 현재는 refresh token 저장에만 쓰고 있습니다(@Cacheable 0건).
측정 해석에 중요한 한계
벤치 데이터에는 AI 파생 행이 없습니다.
ai.context_keyword 전체 213,290행 (전부 옛 시드 것)
회원 3008의 context_keyword 0행
회원 3008의 context 26,666행
즉 위 216ms는 결과가 0건인데도 나온 값입니다. 실제 운영처럼 맥락에 키워드가 붙어 있으면
조인이 행을 만들어 더 비싸질 수도 있고, 반대로 통계가 달라져 플래너가 다른 계획을 고를
수도 있습니다. AI 파생 데이터가 있는 볼륨에서 재측정해야 확정됩니다 — 그 판단과 재측정은
AI 파트 영역이라 제가 하지 않았습니다.
재현 방법
# 천만 건 벤치 볼륨 적재 (약 5분, 디스크 5.9GB)
bash loadtest/tools/run-massive.sh
# 운영과 같은 자원 한도를 얹고 측정
docker compose -p back -f compose.yaml -f compose.bench.yaml up -d postgres
# 회원 규모별 Profile 쿼리 비교 — 괴물 회원은 member 3008~3057 (record 20,000건)
되돌리기는 loadtest/tools/teardown-massive.sql입니다. 자세한 실행법은
loadtest/README.md「대량 벤치」절에 있습니다.
배경 문서
경계에 대해
FeedCandidateRepository·FeedKeywordRepository와 ai 스키마는 AI 파트 소유이고 정본 명세도
그쪽에 있습니다. 저는 관측과 근거만 남깁니다. 캐시 도입은 TTL 동안 새 기록이 취향에
반영되지 않는 트레이드오프를 포함하므로(명세는 이벤트 기반 무효화 없이 TTL만 쓰기로 정하고
있습니다) 정책 결정이 필요한 사안으로 보입니다.
필요하면 재측정이나 벤치 환경 세팅은 도와드릴 수 있습니다.
무엇을 발견했나
FeedKeywordRepository.PROFILE_KEYWORDS_SQL(회원 관심 Keyword 집계)이 회원 본인의 기록 수에정비례하고, 피드 요청마다 실행됩니다.
천만 건 벤치 볼륨(S15P11A705-283, postgres 1Gi 한도)에서 회원 규모별 실측:
record 20,000건 회원의 피드 API가 197ms인데 그중 163ms가 이 쿼리입니다
(S15P11A705-303으로 최신 채널을 236ms → 2.3ms로 줄인 뒤 남은 몫).
FeedService.rank()가 커서 페이지마다 다시 도는 구조라(코드 주석에 명시됨) 스크롤할 때마다반복됩니다.
계획·인덱스로는 못 고칩니다
enable_mergejoin = off로 Nested Loop를 강제해 보면 77ms로 보이지만 8.8만 블록(약 690MB)을만지며
ai.context_keyword를 Parallel Seq Scan으로 두 번 훑습니다. 어느 계획을 골라도 회원의맥락 26,666건 × 키워드 테이블 조인 비용은 남습니다.
즉 쿼리 튜닝 대상이 아니라 호출 빈도를 줄이는 문제로 보입니다.
해법이 이미 명세에 있습니다
docs/ai/spec/feed-profile-cache.md첫 줄:feed:profile:{memberId}feed:coll:{collectionId}TTL 6시간의 근거도 이 쿼리를 두고 쓴 문장으로 읽힙니다 — "Record 하나가 추가돼도 분포가 크게
흔들리지 않으므로 길게 잡습니다. 계산 비용도 상대적으로 큽니다."
Redis는 이미 떠 있고 현재는 refresh token 저장에만 쓰고 있습니다(
@Cacheable0건).측정 해석에 중요한 한계
벤치 데이터에는 AI 파생 행이 없습니다.
즉 위 216ms는 결과가 0건인데도 나온 값입니다. 실제 운영처럼 맥락에 키워드가 붙어 있으면
조인이 행을 만들어 더 비싸질 수도 있고, 반대로 통계가 달라져 플래너가 다른 계획을 고를
수도 있습니다. AI 파생 데이터가 있는 볼륨에서 재측정해야 확정됩니다 — 그 판단과 재측정은
AI 파트 영역이라 제가 하지 않았습니다.
재현 방법
되돌리기는
loadtest/tools/teardown-massive.sql입니다. 자세한 실행법은loadtest/README.md「대량 벤치」절에 있습니다.배경 문서
docs/backend/implements/BI-38-2026-08-03-massive-scale-plan-observation.md— 천만 건 규모 계획 관측docs/ai/spec/feed-profile-cache.md— 미구현 캐시 명세경계에 대해
FeedCandidateRepository·FeedKeywordRepository와ai스키마는 AI 파트 소유이고 정본 명세도그쪽에 있습니다. 저는 관측과 근거만 남깁니다. 캐시 도입은 TTL 동안 새 기록이 취향에
반영되지 않는 트레이드오프를 포함하므로(명세는 이벤트 기반 무효화 없이 TTL만 쓰기로 정하고
있습니다) 정책 결정이 필요한 사안으로 보입니다.
필요하면 재측정이나 벤치 환경 세팅은 도와드릴 수 있습니다.