Skip to content

docs(S15P11A705-125): MVP Feed 후보·점수·Cache에서 Place category·region 제외 - #74

Merged
colosair merged 3 commits into
devfrom
docs/S15P11A705-125-feed-contract
Jul 29, 2026
Merged

docs(S15P11A705-125): MVP Feed 후보·점수·Cache에서 Place category·region 제외#74
colosair merged 3 commits into
devfrom
docs/S15P11A705-125-feed-contract

Conversation

@colosair

@colosair colosair commented Jul 28, 2026

Copy link
Copy Markdown
Member

요약

back#58 §1~§4 선합의 결과를 Feed 명세에 반영합니다. MVP Feed에서 Place category·region을 제외하고, 후보 채널을 4개에서 3개로 줄이며, API 계약을 공용 정본(/api/core/v1/feed/collections, size 20, opaque cursor, 별도 requestId)에 맞춥니다.

문서 전용 PR이며 도메인 코드·DB·마이그레이션 변경은 없습니다. Feed 런타임 구현은 S15P11A705-120의 범위입니다. 같은 티켓에서 나온 published_at DB 불변식은 성격이 달라 별도 PR(#75)로 분리했습니다.

Jira (필수)

관련 GitHub Issue (선택)

배경

back#58이 짚은 대로 Feed 명세와 실제 데이터 모델이 어긋나 있었습니다.

명세가 요구한 것 실제
타인 Collection 특징으로 Place category 사용 core.place에 카테고리 컬럼이 없습니다. 데이터 모델 §2.3이 "값이 비는 경우가 있고 추천은 Keyword 기반"이라는 이유로 저장하지 않기로 했습니다
Profile 상위 region을 후보 쿼리 WHERE 조건으로 사용 데이터 모델 §2.3은 지역 라벨을 "address에서 파생, 표시용, 지역 전용 검색·컬럼 없음"으로 두었습니다. 단위도 동(洞) 대 시·구로 다릅니다
GET /feed?cursor={requestId}:{offset}&size=10 API 명세 §10.1은 /api/core/v1/feed/collections, size 20, opaque cursor, requestId 별도 필드입니다

feed-scoring.mdcategoryOverlap 항과 지역·카테고리 후보 채널이 성립하지 않는 상태였습니다. 컬럼과 인덱스가 없으므로 region 조건은 문자열 함수 기반 전체 스캔이 됩니다.

세 안을 비교했습니다(P42에 표로 기록).

결론
(a) MVP Feed에서 둘 다 제외 채택. DB·Front 계약과 백필이 필요 없고 현재 데이터 모델과 일치합니다
(b) place에 category·region 저장 DTO·DB·Front·백필·갱신 정책이 함께 필요하고, Kakao category_group_code는 비거나 거칠 수 있습니다
(c) 주소 문자열을 후보 SQL에서 파싱 인덱스를 쓸 수 없고 파싱 규칙이 쿼리에 퍼집니다

API 계약은 API 명세가 대외 계약의 단일 원본이므로 그쪽에 맞췄습니다. 커서는 requestId를 커서에 넣는 대신 별도 필드로 유지하고 커서 자체는 opaque로 두었습니다. Front가 requestId를 이벤트 payload에 실어야 하므로(05-1 §1.3) 별도 필드가 편하고, 공통 커서 계약과도 어긋나지 않습니다.

변경 사항

신규

  • docs/ai/proposals/P42-feed-mvp-without-place-metadata.md — 결정 본문입니다. 세 안 비교, 구현 주체(Feed Spring 구현은 이정헌, Team-PinLog/back), FastAPI에 Feed API·점수 계산을 추가하지 않는다는 경계, 감수하는 것(Keyword 없는 사용자의 Cold Start 품질, 기존 4채널 가중치 실험 보류), 향후 확장 조건(placeMeta를 임베딩에 넣으면 embedding_profile을 새 버전으로 올리고 재임베딩하거나 구·신 Profile 병행 후 전환)을 담았습니다.

명세 수정

  • spec/feed-recommendation.md — Feed가 읽는 AI 파생 데이터를 ai.context_keyword·ai.keyword_preset으로 한정했습니다. 후보 채널에서 "지역·카테고리 근접"을 제거하고, 타인 Collection 특징을 PUBLIC Keyword · record_count · published_at으로 교체했습니다. §4 API를 공용 정본 경로·size 20·opaque cursor로 맞췄습니다. Collection 상세는 Feed 전용 URL을 만들지 않고 공통 GET /api/core/v1/collections/{collectionId}를 재사용합니다. AI 처리가 끝나지 않은 Collection도 후보에 포함하고 keywords: []로 응답합니다.
  • spec/feed-scoring.md — 채널을 4개에서 3개로, 배분을 60/60/60/20에서 100/80/20으로 바꿨습니다. 점수 공식에서 w_geo_cat을 제거한 뒤 나머지 세 항을 비례 정규화해 상대 순서를 보존했습니다(0.4/0.3/0.1 → 0.500/0.375/0.125, 합 1.0 유지). geoCategoryAffinity 절 삭제에 따라 3.33.6 절 번호를 3.33.5로 당기고 본문 상호 참조도 함께 고쳤습니다. Cold Start 표는 3항으로(0.5/0.0/0.5) 바꿨습니다. page-size 10→20에 맞춰 후보 풀 근거(20배→10배 여유)·탐색 슬롯·비용 표(DB 4회→3회, 재검증 10건→20건)를 다시 계산했습니다.
  • spec/feed-profile-cache.md — Profile Cache에서 regionWeights·categoryWeights를, Collection Cache에서 regions·categories를 제거했습니다. TTL과 키 형식은 그대로입니다.
  • spec/feed-tests.md — D3(탐색 슬롯)을 10건에서 20건으로, D6(Keyword 0건) 기대치를 "region·category로 점수 산출"에서 "팔로우·최신·무작위로 응답"으로 교체했습니다.
  • spec/feed-event.md — 아래 "최신 dev 기준 재검토"를 참조해 주세요.
  • docs/ai/proposals/README.md — P42를 등재했습니다(AI 소유 표 + 백엔드 관련 전수 표).
  • docs/ai/WORKLOG.md — 3줄 추가했습니다. 합의 반영 / 담당 재배치 정정 / 최신 dev 재검토입니다.

최신 dev 기준 재검토 (S15P11A705-117 반영)

합의 시점 이후 dev가 5커밋 앞서 있었고, 그중 S15P11A705-117(요청 입력 크기 상한 — recordIds 100개·Context 본문 500자)이 계약 규약을 하나 세웠습니다. "서버 방어 상한은 이름 붙은 상수 한 곳(global/common/InputLimits)에 모으고 CursorPage.MAX_SIZE와 같은 값을 쓴다 — 두 기준이 다르면 '서버 방어 상한이 얼마인가'에 답이 둘이 된다."

이 기준으로 Feed 계약을 다시 봤습니다.

항목 결과
size=20 기본값 충돌 없음. CursorPage.DEFAULT_SIZE가 이미 20입니다. 명세에 그 근거를 명시했습니다
size 상한 충돌 없음. 공통 CursorPage.MAX_SIZE(100)와 normalizeSize 보정을 그대로 따르고 Feed 전용 상한을 두지 않는다고 명시했습니다
opaque cursor 충돌 없음. 공통 커서가 이미 Base64(정렬키,id)이고 "클라이언트는 해석하지 않는다"가 규약입니다
POST .../feed/eventsevents 배열 상한 어긋났습니다. 명세가 "배열 크기 상한(예: 50)"으로 미정이었으므로 InputLimits 규약에 맞춰 100(RECORD_IDS_MAX·CursorPage.MAX_SIZE와 동일)으로 고정했습니다
recordIds 100개 · Context 본문 500자 Feed 요청 경로에 해당 입력이 없어 무관합니다

추가로 발견한 내부 불일치도 정리했습니다. feed-recommendation.md §4의 코드블록만 /api/core/v1/...로 갱신되고, 같은 절 본문 · feed-event.md §4 · feed-tests.md E3·E4는 옛 경로 POST /feed/events로 남아 있었습니다. 전부 새 경로로 통일했고 잔존 0건을 확인했습니다.

테스트 / 검증

  • ./gradlew clean check --no-daemonBUILD SUCCESSFUL, 테스트 185건 통과 / 실패 0 / 오류 0. 커밋된 상태에서 최신 origin/dev(cc7753c) 기준으로 실행했습니다. 문서 전용 변경이므로 이 빌드가 보증하는 것은 "기존 동작을 건드리지 않았다"까지입니다. 명세 내용 자체의 정합성은 리뷰로 봐 주셔야 합니다.
  • git diff --check 통과
  • API 계약 변경 시 관련 문서 갱신 — 이 PR이 그 문서 갱신입니다. 다만 팀 공통 docs 레포(static/05_AI_설계.md §14, 08_API_명세.md §10.1)는 아직 반영 전입니다. 아래 "미결 / 후속"을 참조해 주세요.
  • DB 변경 시 PostgreSQL 통합 테스트 — 해당 없음 (DB 변경 없음)
  • migration 변경 시 빈 DB migration 테스트 — 해당 없음 (마이그레이션 변경 없음)
  • 되돌리기 어려운 결정 → P42 추가. 이 결정은 AI 파트 소유 구역이라 BD가 아니라 P 번호를 썼습니다

RED/GREEN 증적을 낼 수 없는 PR입니다. 명세가 서술한 SQL·가중치는 아직 구현이 없어 실행 검증이 불가능합니다. 가중치 합이 1.0인지(0.500+0.375+0.125, Cold Start 0.5+0.0+0.5)와 절 번호·상호 참조는 수기로 확인했습니다. 실행 검증은 S15P11A705-120에서 feed-tests.md의 케이스를 테스트로 옮길 때 이뤄집니다.

리뷰 포인트

  1. 가중치 비례 정규화가 의도대로인지 봐 주세요. w_geo_cat(0.2)만 빼고 남은 0.8을 세 항에 비례 배분해 0.500/0.375/0.125로 만들었습니다. 상대 순서와 비율은 보존되지만, category·region이 없어진 만큼 w_keywordw_recency의도적으로 더 올리는 선택지도 있었습니다. 비례 유지를 택한 이유는 이번 PR을 "빼기"에 한정하고 가중치 튜닝은 실제 이벤트 집계를 보고 하기 위해서입니다.

  2. 채널 배분 100/80/20의 근거가 얇습니다. 기존 60/60/60/20에서 지역·카테고리 몫 60을 최신(+40)·팔로우(+20)로 나눠 총합 200을 유지했습니다. 후보 풀 크기 200과 맞춘 것 외에 강한 근거는 없습니다. 팔로우가 없는 사용자는 사실상 최신+무작위 120건 풀이 됩니다.

  3. 이벤트 배열 상한을 100으로 고정한 판단을 봐 주세요. "예: 50"을 그대로 둘 수도 있었지만, 미정 상태로 두면 구현 시점에 또 다른 숫자가 나옵니다. S15P11A705-117이 세운 "답은 하나" 규약을 따라 CursorPage.MAX_SIZE와 같은 값으로 맞췄습니다. 이벤트는 한 페이지(20건)에서 나오는 CLICK·SAVE이므로 100이면 충분하다고 봤는데, 별도 값이 맞다고 보시면 조정하겠습니다.

  4. WORKLOG에 담당 재배치 2줄이 연속으로 들어갑니다. "구현 담당 김가현" 직후 "긴급 정정 — 이정헌에게 재배치"입니다. 한 커밋에 오답과 정정이 같이 들어가는 모양이라 정정 줄만 남길 수도 있었지만, 재배치가 실제로 있었던 사실이므로 WORKLOG의 append-only 성격에 맞춰 둘 다 남겼습니다. 정리하는 편이 낫다면 그렇게 하겠습니다.

  5. GET /feed/collections/{id}/shelf는 이 PR에서 손대지 않았습니다. back#58 댓글이 "shelf는 AI 의존이 없어 §2~§4를 기다리지 않고 먼저 끊을 수 있다"고 분석했는데, 그 선행 조건인 back#63(공개 조회 서비스 통합)이 별도 이슈로 살아 있어 이 PR 범위 밖으로 두었습니다.

미결 / 후속

  • 팀 공통 docs 레포 반영이 남았습니다. static/05_AI_설계.md §14.2(타인 Collection 특징에 Place category 사용)와 08_API_명세.md §10.1이 정본이므로, 이 PR이 승인되면 그쪽 PR을 별도로 올려야 합니다. 그 전까지는 정본과 이 명세가 어긋난 상태입니다.
  • back#58 §5 published_at은 짝 PR #75에서 다룹니다.
  • back#58 §6 record_count 불일치 탐지는 이슈가 "지금 조치 불필요"로 둔 관찰 항목이라 손대지 않았습니다. Feed 도입이 BD-20의 재검토 트리거를 앞당길 수 있다는 점만 남아 있습니다.
  • Feed Spring 구현(후보 채널 SQL·점수 계산·Session Redis 보관·feed-tests.md 케이스)은 S15P11A705-120입니다.
  • back#63(공개·소유자 경계 통합)은 이 PR에 흡수하지 않았습니다.

🤖 Generated with Claude Code

@colosair
colosair requested a review from minyongP July 28, 2026 10:12
@colosair
colosair force-pushed the docs/S15P11A705-125-feed-contract branch from 9eb63e6 to b202e5c Compare July 28, 2026 10:23
@colosair colosair changed the title docs(S15P11A705-125): MVP Feed 계약에서 Place category·region을 걷어낸다 docs(S15P11A705-125): MVP Feed 후보·점수·Cache에서 Place category·region 제외 Jul 28, 2026
back#58 선합의 결과를 미커밋 상태로 두지 않고 최신 dev 기준으로 외부화합니다.
Feed 명세가 Place category·region을 후보 채널·점수 입력으로 쓰고 있었지만
core.place는 두 값을 저장하지 않아 명세와 데이터 모델이 어긋나 있었습니다.

- P42 — MVP에서 category·region을 제외하는 결정과 (a)/(b)/(c) 대안 비교,
  향후 placeMeta 임베딩 도입 시 embedding_profile 전환 비용을 함께 기록했습니다
- feed-recommendation — 후보 채널을 4개에서 3개로 줄이고, 타인 Collection 특징을
  PUBLIC Keyword·record_count·published_at으로 한정했으며, API 경로를 공용
  정본(/api/core/v1)에 맞췄습니다
- feed-scoring — w_geo_cat을 제거한 뒤 나머지 세 항을 비례 정규화하고
  (0.5/0.375/0.125), 채널 배분을 100·80·20으로, page-size 10→20에 맞춰
  다양성·비용 수치를 다시 계산했습니다
- feed-profile-cache — Profile·Collection Cache에서 region·category 필드를
  제거했습니다
- feed-tests — D3·D6 기대치를 3채널 Cold Start 기준으로 교체했습니다

최신 dev 재검토(S15P11A705-117 입력 크기 상한):
size=20·opaque cursor는 CursorPage 공통 계약과 이미 일치해 그 근거를 명세에
명시했고, 미정이던 이벤트 배열 상한("예: 50")은 InputLimits 규약에 맞춰 100으로
고정했습니다. §4 코드블록만 새 경로로 바뀌고 본문·feed-event·feed-tests는 옛
POST /feed/events로 남아 있던 불일치도 함께 정리했습니다.

Feed 런타임 구현은 S15P11A705-120의 범위이며 이 커밋에 없습니다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@colosair

Copy link
Copy Markdown
Member Author

정본 반영 PR을 열었습니다 — Team-PinLog/docs#21 (Draft).

이 PR이 확정한 계약(후보 3채널·배분 100/80/20, 가중치 0.500/0.375/0.125, 이벤트 배열 상한 100, requestId 별도 필드)을 팀 공용 정본 static/05_AI_설계.md §14·static/08_API_명세.md §10에 옮겼습니다. S15P11A705-119.

병합 순서: 이 PR(back#74)이 먼저입니다. 리뷰에서 계약이 바뀌면 docs#21을 먼저 갱신한 뒤 병합합니다.

@minyongP minyongP left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빼는 결정 자체는 타당하다고 봅니다. core.place에 없는 컬럼을 명세가 WHERE 조건으로 쓰고 있었으니 (b)·(c)는 비용 대비 얻는 게 적습니다.

검증한 것부터 적습니다.

  • 비례 정규화 — 0.4/0.3/0.1 ÷ 0.8 = 0.500/0.375/0.125, 합 1.0. Cold Start 0.5+0.0+0.5 = 1.0. 맞습니다.
  • 코드 인용 — CursorPage.DEFAULT_SIZE=20, CursorPage.MAX_SIZE=100, InputLimits.RECORD_IDS_MAX=100 실제 값과 일치합니다.
  • 옛 경로 잔존 — docs/ 전체에서 POST /feed/events·GET /feed? 0건 확인했습니다.
  • 절 번호 3.3~3.5 재정렬과 본문 상호 참조(3.5→3.4)도 어긋난 곳 없습니다.

두 가지만 반영을 부탁드립니다. 둘 다 "빼기"로 보이지만 실제로는 추천 품질 정책이 바뀐 지점이라, 판단이 문서에 남아야 나중에 추적됩니다.


1. 탐색 슬롯 비율이 절반으로 줄어듭니다 (feed-scoring.md §4.2, §6)

page-size를 10에서 20으로 올리면서 exploration-slots: 2를 그대로 두셨습니다. 탐색 비중이 **20% → 10%**가 됩니다. Cold Start도 기본 3 유지라 **30% → 15%**입니다. 무작위 채널 배분도 20 그대로여서 후보 풀 200의 10%입니다.

명세는 탐색 슬롯의 목적을 "순수 exploit만 하면 Profile이 자기 강화되어 새로운 취향을 발견할 수 없다"로 적어두었는데, 그 강도를 절반으로 낮춘 판단은 diff 어디에도 없습니다. PR 본문 리뷰 포인트 5개에도 빠져 있어서, page-size 반영의 부수 효과로 묻힌 것으로 보입니다.

의도한 변경이면 그 근거를 §4.2에 한 줄 남겨 주시고, 아니라면 비율을 유지하는 쪽(일반 4, Cold Start 6)으로 올리는 게 맞다고 봅니다.

2. AI 미완료 Collection이 점수를 받을 경로가 사라졌습니다 (feed-scoring.md §3.3 삭제)

삭제된 §3.3에 이 문장이 있었습니다.

region은 Place 주소에서 파생한 행정구역 단위(시·구)를 사용합니다. Keyword가 아니라 Place metadata이므로 AI 완료 여부와 무관하게 항상 계산할 수 있습니다. AI가 미완료인 Collection도 이 항으로 점수를 얻습니다.

이 역할을 무엇이 대신하는지가 명세에 없습니다. 이제 keywordAffinity = 0이므로, 팔로우하지 않은 갓 발행된 Collection은 recency(w=0.125) 하나로 경쟁합니다.

feed-recommendation.md §4에 새로 넣으신 "AI 처리가 끝나지 않은 Collection도 후보에 포함하며 keywords: []를 넣습니다"는 후보 포함을 보장할 뿐 노출을 보장하지 않습니다. 둘은 다른 층위입니다.

최신 채널 배분을 60에서 100으로 올린 것이 사실상 이 자리를 메우는 보상으로 읽히는데, 그 인과가 문서에 연결돼 있지 않습니다. P42 "감수하는 것"에도 사용자 쪽 Cold Start만 있고 Collection 쪽 Cold Start(신규 발행분의 노출 지연) 는 빠져 있습니다. 둘 중 하나를 부탁드립니다.

  • 최신 채널 증량이 이 손실을 메우는 조치임을 §2.1이나 P42에 명시
  • 또는 P42 "감수하는 것"에 "AI 미완료 Collection은 팔로우·최신 경로로만 노출된다" 한 줄 추가

아래는 반영 여부를 맡기는 항목입니다.

3. feed-tests.md에 이번에 고정한 계약이 없습니다. D3의 숫자만 10에서 20으로 바뀌었습니다. 새로 못박은 계약 네 가지 — size 기본 20 / 상한 100 / normalizeSize 보정, opaque cursor 왕복, events 배열 100개 초과 거부, requestId 별도 필드 — 가 테스트 표에 한 줄도 없습니다. 명세로 계약을 세우셨으니 표에도 넣어두면 S15P11A705-120에서 빠지지 않습니다. 특히 배열 상한 초과 응답 코드가 이 문서엔 없고 docs#21에만 400 INVALID_INPUT으로 있습니다. E5가 204를 기대하는 것과 나란히 두면 좋겠습니다.

4. InputLimits 상수 이름을 정해 주세요. feed-event.md §4가 "이름 붙은 상수로 둔다"까지만 적고 이름을 비워두었습니다. RECORD_IDS_MAX처럼 FEED_EVENTS_MAX를 명세에 박아두면 구현 시점에 다시 고민하지 않습니다. S15P11A705-117이 세운 규약의 취지도 그쪽입니다.

5. 후보 풀 200과 채널 배분 합 200이 같아 잘라내기 규칙이 발동할 수 없습니다. 중복 제거 후에는 항상 200 미만입니다. 이전 60+60+60+20도 같은 구조였지만, 이번에 근거 문장을 "20배 → 10배 여유"로 다시 계산해 적으셨으니 같이 보면 좋겠습니다. 팔로우가 0인 사용자는 최신 100 + 무작위 20 = 120이고 중복 제거 후에는 더 줄어, 20건을 뽑기에 6배 남짓입니다.

6. 가중치 표기를 통일해 주세요. §7 Cold Start 표에서 일반 열은 0.500/0.375/0.125인데 Cold Start 열은 0.5/0.0/0.5입니다. 같은 w_follow0.5000.5로 갈립니다. YAML로 옮길 때 헷갈립니다.

7. 이 PR 범위 밖 발견입니다. 정본 static/08_API_명세.md §8.1의 GET /api/core/v1/feed/collections/{collectionId}/shelf가 아직 size=10입니다. docs#21이 이 줄을 건드리지 않아서, 네 PR이 모두 머지돼도 feed/collections는 20, 같은 Feed 네임스페이스의 shelf는 10으로 남습니다. 이 PR이 고치고 있는 것과 같은 종류의 불일치라, docs#21에 함께 넣을지 판단 부탁드립니다.


머지 순서(back#74 → docs#21)는 코멘트대로 동의합니다. #75와 docs#22도 같은 이유로 순서를 지키는 게 맞습니다.

…노출 손실 명시

minyongP의 CHANGES_REQUESTED 리뷰 7항목 중 back 소관 6항목을 반영합니다.
정본 shelf size(7번)는 docs#21에서 처리합니다.

1. 탐색 슬롯 비율 원복 (feed-scoring.md §4·§4.2·§5.2)
   page-size를 10에서 20으로 올리면서 exploration-slots를 그대로 둬 탐색
   비중이 20%에서 10%로 절반이 됐습니다. 슬롯을 일반 2→4, Cold Start 3→6으로
   올려 비율을 되돌립니다. 정책을 바꾼 적이 없으므로 원복이며, 슬롯 수가
   page-size에 비례한다는 근거를 §4.2에 남깁니다. feed-tests.md D3도 맞춥니다.

2. AI 미완료 Collection 노출 손실 (feed-scoring.md §2.1, P42)
   Place region 항이 빠지면서 AI 완료 여부와 무관하게 점수를 얻던 경로가
   사라졌습니다. 최신 채널 배분 60→100이 이에 대한 조치임을 §2.1에 밝히되,
   후보 진입만 넓힐 뿐 점수 열세는 남는 부분적 보상임을 함께 적습니다.
   P42 "감수하는 것"에 Collection 쪽 Cold Start를 추가합니다.

3. 이번에 고정한 계약을 테스트 표에 반영 (feed-tests.md)
   §11 Q1~Q8로 size 기본 20·상한 100·normalizeSize 보정, opaque cursor 왕복,
   requestId 별도 필드를 고정합니다. 배열 상한 초과는 E9로 E5(204)와 같은
   표에 두어 부분 무효와 크기 위반의 응답이 갈리는 지점을 드러냅니다.

4. FEED_EVENTS_MAX 이름 확정 (feed-event.md §4)
   "이름 붙은 상수" 자리를 RECORD_IDS_MAX 관례에 맞춰 채우고, 초과 시
   400 INVALID_INPUT으로 거부한다는 응답 코드를 함께 못박습니다.

5. 후보 풀 잘라내기가 죽은 규칙임을 명시 (feed-scoring.md §2.3)
   채널 배분 합(200)이 pool-size와 같아 중복 제거 후 항상 200 이하이므로
   잘라내기가 발동할 수 없습니다. "10배 여유" 문장도 실제에 맞춰 고칩니다 —
   팔로우 0인 사용자는 최신 100 + 무작위 20에서 시작해 6배 남짓입니다.

6. 가중치 표기 통일 (feed-scoring.md §5.2)
   Cold Start 열의 0.5/0.0/0.5를 0.500/0.000/0.500으로 맞춥니다. 값 변경은
   없습니다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@colosair

Copy link
Copy Markdown
Member Author

리뷰 감사합니다. 7항목 전부 반영했습니다.

먼저 검증해주신 네 가지부터. 비례 정규화 재계산(0.4/0.3/0.1 ÷ 0.8), 코드 상수 대조(CursorPage.DEFAULT_SIZE·MAX_SIZE·InputLimits.RECORD_IDS_MAX), docs/ 전체 옛 경로 0건 확인, 절 번호 재정렬과 본문 상호 참조 — 네 항목 모두 직접 확인해주신 덕분에 이번 반영에서 다시 볼 필요가 없었습니다. 특히 옛 경로 잔존은 제가 스스로 놓쳤을 확률이 높은 지점이었습니다.

반영 커밋은 back#74 796d02e(1~6번)와 docs#21 6439ed0(7번)입니다.


1. 탐색 슬롯 비율 — 원복했습니다

의도한 변경이 아니었습니다. page-size를 10에서 20으로 올리면서 exploration-slots가 따라가지 않은 누락이고, 말씀하신 대로 비중이 20% → 10%, Cold Start가 30% → 15%로 절반이 됐습니다.

개수를 두 배로 올려 비율을 유지합니다 — 일반 2 → 4, Cold Start 3 → 6. 근거를 §4.2에 한 줄 남기라고 하셨는데, 대신 슬롯 수가 page-size에 비례한다는 규칙 자체를 적었습니다. 이번 사고가 "값 하나를 안 따라 올렸다"라서, 같은 일이 다음 page-size 조정에서 반복되지 않으려면 개별 근거보다 관계를 박아두는 쪽이 낫다고 봤습니다. feed-tests.md D3도 4건으로 맞췄습니다.

무작위 채널 배분 20은 그대로 둡니다. 이 값은 탐색 슬롯과 다른 축(후보 진입)이고, 슬롯 4개를 채우는 데 후보 20개면 충분해서 비율로 묶이지 않습니다.

2. AI 미완료 Collection — 두 곳 모두 적었습니다

"둘 중 하나"라고 하셨지만 둘 다 넣었습니다. 인과와 한계가 서로 다른 문서에 있어야 추적이 됩니다.

  • feed-scoring.md §2.1 — 최신 채널 60 → 100이 region 제거에 따른 조치임을 밝히고, 부분적 보상임을 함께 적었습니다. 채널 배분은 후보 풀 진입만 결정하고 점수에는 관여하지 않으므로 keywordAffinity = 0인 Collection의 점수 열세는 그대로 남습니다.
  • P42 "감수하는 것"Collection 쪽 Cold Start를 추가했습니다. "후보 포함과 노출은 다른 층위"라는 지적이 정확해서 그 구분을 거의 그대로 옮겼습니다. recency(w=0.125) 하나로 경쟁한다는 것과, 실질 노출 경로가 팔로우·최신 두 갈래로 좁아진다는 것까지 적었습니다.

"보상했으니 해결됐다"로 읽히지 않게 쓰는 것이 이 항목의 핵심이라고 봤습니다.

3. feed-tests.md — §11과 E9로 추가했습니다

새 절 §11에 Q1Q8을 뒀습니다. size 기본 20·상한 100·normalizeSize 보정(Q1Q3), opaque cursor 왕복과 불투명성(Q4Q6), requestId 별도 필드(Q7Q8)입니다.

배열 상한 초과는 E9로 이벤트 표에 넣어 E5와 같은 표에 뒀고, 응답은 400 INVALID_INPUT입니다. 표 아래에 둘을 함께 봐야 하는 이유를 적었습니다 — 개별 항목의 무효는 조용히 버리고 204(E5), 요청 전체의 크기 위반은 400(E9) 으로 갈리는 지점이라 한쪽만 검증하면 구현이 어느 쪽으로든 흘러갑니다.

Q1~Q3은 Feed 컨트롤러가 자체 상한을 도입하면 실패하도록 의도했습니다. S15P11A705-117 규약이 깨지는 지점을 문서가 아니라 테스트로 잡아두는 편이 낫다고 봤습니다.

4. FEED_EVENTS_MAX — 제안하신 이름 그대로 박았습니다

RECORD_IDS_MAX<대상 배열>_MAX 관례를 따른다는 근거도 함께 적었습니다. 초과 시 400 INVALID_INPUT으로 거부하며 초과분만 잘라내 저장하지 않는다는 것도 명세에 넣었습니다. 조용한 유실은 클라이언트가 인지할 방법이 없습니다.

5. 잘라내기 규칙 — 죽은 규칙임을 명시했습니다

맞습니다. 배분 합(100 + 80 + 20)이 pool-size와 같은 200이고 중복 제거는 개수를 줄이기만 하므로 합집합은 항상 200 이하입니다. 규칙을 지우는 대신 배분 합을 pool-size보다 크게 올릴 때를 대비한 방어선으로 남긴다고 성격을 적었습니다.

"10배 여유" 문장은 주신 계산을 그대로 반영했습니다. 200은 상한이지 확보량이 아니고, 팔로우 0인 사용자는 최신 100 + 무작위 20 = 120에서 시작해 중복 제거 후 더 줄어 6배 남짓입니다. 이 정도면 다양성 조정과 재검증 탈락을 흡수한다고 판단해 배분 값 자체는 유지했습니다.

6. 가중치 표기 — 소수점 세 자리로 통일했습니다

0.500 / 0.000 / 0.500. 값 변경은 없습니다.

7. 정본 shelf size — docs#21에 함께 넣었습니다

static/08_API_명세.md §8.1을 size=20으로 고쳤습니다. 같은 Feed 네임스페이스에서 size가 갈리는 문제라 이 시리즈가 고치고 있는 것과 같은 종류가 맞습니다. size가 공통 커서 계약(기본 20 · 상한 100 · normalizeSize 보정)을 따른다는 근거도 함께 적었습니다.

같은 파일의 GET /api/core/v1/collections?...&size=10도 눈에 띄었지만 Feed 네임스페이스 밖이고 이 시리즈의 범위가 아니라 건드리지 않았습니다. 별도로 볼 문제로 남깁니다.


소유 경계 — 반영과 별개로 한 가지

반영 여부와 무관한 절차 이야기라, 다음을 위해 적어둡니다.

S15P11A705-119에서 확정한 대로 Feed의 정책·계약은 AI 파트 단독 소유입니다. docs/ai/**의 점수 공식·가중치·후보 채널·탐색 슬롯·Cold Start 판정은 AI 파트가 결정하며, 백엔드 리뷰는 참고 의견이지 승인 게이트가 아닙니다.

이번 7항목을 반영한 것은 지적이 타당했기 때문이지 승인을 얻어야 해서가 아닙니다. 1번은 제가 놓친 누락이고, 2번은 인과가 문서에 연결돼 있지 않다는 지적이 맞고, 5번은 실제로 발동할 수 없는 규칙입니다. 절차가 아니라 내용이 옳아서 받았습니다.

이 구분을 남기는 이유는, 다음에 정책 판단이 갈릴 때 AI 파트 결정이 백엔드 승인 대기에 걸리는 선례를 만들지 않기 위해서입니다. 예를 들어 탐색 비중을 20%로 둘지 10%로 둘지가 실제로 판단이 갈리는 사안이었다면, 결정권은 AI 파트에 있고 리뷰는 그 판단의 입력이 됩니다. 이번엔 갈릴 여지가 없던 사안이라 그 경계가 시험되지 않았을 뿐입니다.

접점은 둘이고, 이번에는 둘 다 충돌이 없었습니다.

  • FEED_EVENTS_MAXInputLimits가 back 코드의 클래스라 이름이 곧 구현을 구속합니다. 명세가 일방적으로 정할 값이 아니어서 제안하신 이름을 그대로 받았습니다.
  • 정본 static/08_API_명세.md — 팀 공용 문서라 AI 파트 단독 소유가 아닙니다. 7번을 docs/ai/**가 아니라 docs#21에 넣은 것도 그 성격 때문입니다.

머지 순서(back#74 → docs#21, #75 → docs#22) 동의 감사합니다.

@minyongP
minyongP self-requested a review July 29, 2026 01:46

@minyongP minyongP left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@colosair
colosair merged commit ad676b8 into dev Jul 29, 2026
2 checks passed
@colosair
colosair deleted the docs/S15P11A705-125-feed-contract branch July 29, 2026 02:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants