docs(S15P11A705-125): MVP Feed 후보·점수·Cache에서 Place category·region 제외 - #74
Conversation
9eb63e6 to
b202e5c
Compare
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>
b202e5c to
4d3769c
Compare
|
정본 반영 PR을 열었습니다 — Team-PinLog/docs#21 (Draft). 이 PR이 확정한 계약(후보 3채널·배분 100/80/20, 가중치 0.500/0.375/0.125, 이벤트 배열 상한 100, 병합 순서: 이 PR(back#74)이 먼저입니다. 리뷰에서 계약이 바뀌면 docs#21을 먼저 갱신한 뒤 병합합니다. |
minyongP
left a comment
There was a problem hiding this comment.
빼는 결정 자체는 타당하다고 봅니다. 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_follow가 0.500과 0.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>
|
리뷰 감사합니다. 7항목 전부 반영했습니다. 먼저 검증해주신 네 가지부터. 비례 정규화 재계산(0.4/0.3/0.1 ÷ 0.8), 코드 상수 대조( 반영 커밋은 back#74 1. 탐색 슬롯 비율 — 원복했습니다의도한 변경이 아니었습니다. 개수를 두 배로 올려 비율을 유지합니다 — 일반 무작위 채널 배분 20은 그대로 둡니다. 이 값은 탐색 슬롯과 다른 축(후보 진입)이고, 슬롯 4개를 채우는 데 후보 20개면 충분해서 비율로 묶이지 않습니다. 2. AI 미완료 Collection — 두 곳 모두 적었습니다"둘 중 하나"라고 하셨지만 둘 다 넣었습니다. 인과와 한계가 서로 다른 문서에 있어야 추적이 됩니다.
"보상했으니 해결됐다"로 읽히지 않게 쓰는 것이 이 항목의 핵심이라고 봤습니다. 3.
|
요약
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_atDB 불변식은 성격이 달라 별도 PR(#75)로 분리했습니다.Jira (필수)
S15P11A705-125· https://ssafy.atlassian.net/browse/S15P11A705-125관련 GitHub Issue (선택)
published_atDB 불변식 = back#58 §5)배경
back#58이 짚은 대로 Feed 명세와 실제 데이터 모델이 어긋나 있었습니다.
category사용core.place에 카테고리 컬럼이 없습니다. 데이터 모델 §2.3이 "값이 비는 경우가 있고 추천은 Keyword 기반"이라는 이유로 저장하지 않기로 했습니다region을 후보 쿼리 WHERE 조건으로 사용address에서 파생, 표시용, 지역 전용 검색·컬럼 없음"으로 두었습니다. 단위도 동(洞) 대 시·구로 다릅니다GET /feed?cursor={requestId}:{offset}&size=10/api/core/v1/feed/collections, size 20, opaque cursor,requestId별도 필드입니다즉
feed-scoring.md의categoryOverlap항과 지역·카테고리 후보 채널이 성립하지 않는 상태였습니다. 컬럼과 인덱스가 없으므로 region 조건은 문자열 함수 기반 전체 스캔이 됩니다.세 안을 비교했습니다(P42에 표로 기록).
place에 category·region 저장category_group_code는 비거나 거칠 수 있습니다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 특징을PUBLICKeyword ·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(요청 입력 크기 상한 —recordIds100개·Context 본문 500자)이 계약 규약을 하나 세웠습니다. "서버 방어 상한은 이름 붙은 상수 한 곳(global/common/InputLimits)에 모으고CursorPage.MAX_SIZE와 같은 값을 쓴다 — 두 기준이 다르면 '서버 방어 상한이 얼마인가'에 답이 둘이 된다."이 기준으로 Feed 계약을 다시 봤습니다.
size=20기본값CursorPage.DEFAULT_SIZE가 이미 20입니다. 명세에 그 근거를 명시했습니다size상한CursorPage.MAX_SIZE(100)와normalizeSize보정을 그대로 따르고 Feed 전용 상한을 두지 않는다고 명시했습니다Base64(정렬키,id)이고 "클라이언트는 해석하지 않는다"가 규약입니다POST .../feed/events의events배열 상한InputLimits규약에 맞춰 100(RECORD_IDS_MAX·CursorPage.MAX_SIZE와 동일)으로 고정했습니다recordIds100개 · Context 본문 500자추가로 발견한 내부 불일치도 정리했습니다.
feed-recommendation.md§4의 코드블록만/api/core/v1/...로 갱신되고, 같은 절 본문 ·feed-event.md§4 ·feed-tests.mdE3·E4는 옛 경로POST /feed/events로 남아 있었습니다. 전부 새 경로로 통일했고 잔존 0건을 확인했습니다.테스트 / 검증
./gradlew clean check --no-daemon— BUILD SUCCESSFUL, 테스트 185건 통과 / 실패 0 / 오류 0. 커밋된 상태에서 최신origin/dev(cc7753c) 기준으로 실행했습니다. 문서 전용 변경이므로 이 빌드가 보증하는 것은 "기존 동작을 건드리지 않았다"까지입니다. 명세 내용 자체의 정합성은 리뷰로 봐 주셔야 합니다.git diff --check통과docs레포(static/05_AI_설계.md§14,08_API_명세.md§10.1)는 아직 반영 전입니다. 아래 "미결 / 후속"을 참조해 주세요.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의 케이스를 테스트로 옮길 때 이뤄집니다.리뷰 포인트
가중치 비례 정규화가 의도대로인지 봐 주세요.
w_geo_cat(0.2)만 빼고 남은 0.8을 세 항에 비례 배분해 0.500/0.375/0.125로 만들었습니다. 상대 순서와 비율은 보존되지만, category·region이 없어진 만큼w_keyword나w_recency를 의도적으로 더 올리는 선택지도 있었습니다. 비례 유지를 택한 이유는 이번 PR을 "빼기"에 한정하고 가중치 튜닝은 실제 이벤트 집계를 보고 하기 위해서입니다.채널 배분 100/80/20의 근거가 얇습니다. 기존 60/60/60/20에서 지역·카테고리 몫 60을 최신(+40)·팔로우(+20)로 나눠 총합 200을 유지했습니다. 후보 풀 크기 200과 맞춘 것 외에 강한 근거는 없습니다. 팔로우가 없는 사용자는 사실상 최신+무작위 120건 풀이 됩니다.
이벤트 배열 상한을 100으로 고정한 판단을 봐 주세요. "예: 50"을 그대로 둘 수도 있었지만, 미정 상태로 두면 구현 시점에 또 다른 숫자가 나옵니다. S15P11A705-117이 세운 "답은 하나" 규약을 따라
CursorPage.MAX_SIZE와 같은 값으로 맞췄습니다. 이벤트는 한 페이지(20건)에서 나오는 CLICK·SAVE이므로 100이면 충분하다고 봤는데, 별도 값이 맞다고 보시면 조정하겠습니다.WORKLOG에 담당 재배치 2줄이 연속으로 들어갑니다. "구현 담당 김가현" 직후 "긴급 정정 — 이정헌에게 재배치"입니다. 한 커밋에 오답과 정정이 같이 들어가는 모양이라 정정 줄만 남길 수도 있었지만, 재배치가 실제로 있었던 사실이므로 WORKLOG의 append-only 성격에 맞춰 둘 다 남겼습니다. 정리하는 편이 낫다면 그렇게 하겠습니다.
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을 별도로 올려야 합니다. 그 전까지는 정본과 이 명세가 어긋난 상태입니다.published_at은 짝 PR #75에서 다룹니다.record_count불일치 탐지는 이슈가 "지금 조치 불필요"로 둔 관찰 항목이라 손대지 않았습니다. Feed 도입이 BD-20의 재검토 트리거를 앞당길 수 있다는 점만 남아 있습니다.feed-tests.md케이스)은 S15P11A705-120입니다.🤖 Generated with Claude Code