Files
llm-wiki/raw/branch-notes/experiment-nplus1-feed-api-replay.md
T

22 KiB

title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, branch, git_branch, parent_branch, related_projects, tags, created, target_merge, status_label, evidence_grade, contract_packet_sha256
title source_type status id kind project work_item inherits refines overrides depends_on contract_packet branch git_branch parent_branch related_projects tags created target_merge status_label evidence_grade contract_packet_sha256
branch / experiment-nplus1-feed-api-replay (11-stage real DB and HTTP replay) branch-note raw BR-NPLUS1-PRESENTATION-PREP-002 project-work-item nplus1-presentation-prep WI-NPLUS1-PRESENTATION-PREP-002
DEC-NPLUS1-PRESENTATION-PREP-LEARNING-001@1
DEC-NPLUS1-PRESENTATION-PREP-SUBSTRATE-001@1
DEC-NPLUS1-PRESENTATION-PREP-EVIDENCE-001@1
DEC-NPLUS1-PRESENTATION-PREP-SCOPE-001@1
WI-NPLUS1-PRESENTATION-PREP-001
1 experiment-nplus1-feed-api-replay lab/nplus1-api-replay
nplus1-presentation-prep
ca-skeleton
branch
nplus1-presentation-prep
persistence
testing
api-design
postgresql
hands-on-lab
2026-07-15 review locally-verified 88f5e6fd5ec219c6017f8213d076cde1803e63e62f978fe926f86ddcbc616f41

branch: experiment-nplus1-feed-api-replay

Layer: raw/branch-notes/ — 기존 raw/branch-notes/experiment-nplus1-highlight-feed의 L1~L16/Crown/L12 결과를, 실제 PostgreSQL과 HTTP로 한 단계씩 재현할 수 있게 만든 11-checkpoint replay 브랜치다. 실제 Git branch는 lab/nplus1-api-replay다. wiki slug는 파일명 규칙에 맞춘 별도 식별자다.

부모 (필수)

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: 11개 replay tag와 guide mapping을 고정하고, clean clone/worktree에서 Compose·HTTP·PostgreSQL smoke 및 full-check 상태를 재검증하며 lab profile의 deployment 비활성 증거를 기록한다.

상속한 프로젝트 결정

Decision Ref Project Summary Branch Application Source
DEC-NPLUS1-PRESENTATION-PREP-LEARNING-001@1 Measure→Break→Diagnose→Fix→Re-measure→Generalize를 랩 완료 루프로 사용한다. 11개 checkout point마다 동일한 reset→HTTP→DB 관찰 절차를 제공한다. raw/project-notes/nplus1-presentation-prep
DEC-NPLUS1-PRESENTATION-PREP-SUBSTRATE-001@1 ca-tmpl production substrate를 사용하고 학습 API·측정 경로는 profile과 sibling 경로로 격리한다. /api/lab/**와 marker-owned fixture를 lab profile에 한정한다. raw/project-notes/nplus1-presentation-prep
DEC-NPLUS1-PRESENTATION-PREP-EVIDENCE-001@1 local·Testcontainers 결과는 locally-verified로만 기록하고 prod evidence로 승격하지 않는다. Compose/Testcontainers 결과를 locally-verified로만 기록한다. raw/project-notes/nplus1-presentation-prep
DEC-NPLUS1-PRESENTATION-PREP-SCOPE-001@1 same-store CQRS-lite까지를 현재 범위로 두고 full CQRS는 ca-tmpl contract escalation 이후에만 허용한다. Crown 1-query와 L12 2-query endpoint를 병존시킨다. raw/project-notes/nplus1-presentation-prep

브랜치 지역 결정

기존 branch-local 결정은 아래 ## Decision Evidence Map의 D1~D5가 소유한다.

선언한 예외

  • 없음.

목표

마지막 Crown/L12 상태만 남은 작업 트리에서는 L1의 컬렉션 N+1부터 Crown의 통합 쿼리까지를 HTTP와 실제 DB로 순서대로 관찰하기 어렵다. 이를 11개의 독립 checkout point로 고정한다.

  • 각 tag에서 Docker PostgreSQL을 띄우고 lab fixture를 reset한 뒤 API 응답과 DB row를 직접 확인한다.

  • 정상 /api/feed 동작은 바꾸지 않고, lab profile에서만 학습용 /api/lab/** 경로를 제공한다.

  • Crown의 1-query read와 L12의 same-store CQRS-lite 2-query read를 같은 것으로 포장하지 않고, 별도 endpoint와 문서로 비교 가능하게 둔다.

  • 이슈: 사용자 요청 — N+1 랩을 실 API/DB로 단계별 학습

  • PR: 없음 (로컬 replay branch)

범위

포함 범위

  • L1, L2, L3, L4, L5, L6, L14, L15, L16, Crown, L12 순서의 정확히 11개 commit/tag.
  • lab profile, marker-safe fixture, HTTP 관찰 endpoint, PostgreSQL 확인 절차와 단계별 가이드.
  • Crown 및 L12의 실제 PostgreSQL/Testcontainers 검증과 이력 tag의 L1 smoke 검증.

제외 범위

  • /api/feed의 production 계약 또는 기본 보안 정책 변경.
  • L12를 별도 read store·outbox 동기화가 있는 Full CQRS로 확장.
  • 프로덕션 배포, 부하/latency SLA, 성능 수치의 운영 일반화.

근거

Source 정당화하는 결정
raw/official-docs/cqrs-pattern-azure-architecture-center D3: 같은 저장소에서 read/write 논리를 분리하는 CQRS-lite와 별도 저장소 CQRS를 구분한다.
raw/official-docs/spring-data-jpa-projections-spring-official D3: application 반환용 DTO/projection을 통해 aggregate hydration과 read shape를 분리하는 선택지를 뒷받침한다.
raw/official-docs/test-taxonomy-testcontainers-official D4: in-memory 대체물이 아닌 Docker의 실제 PostgreSQL로 integration evidence를 얻는 선택을 뒷받침한다.

TODO

  • 11개 replay commit/tag를 사용자 지정 학습 순서로 고정 — 등급: actually-implemented
  • lab profile에서 실제 DB reset 및 HTTP feed 관찰 경로 제공 — 등급: actually-implemented
  • Crown/L12 최신 상태의 Docker HTTP + PostgreSQL smoke 수행 — 등급: locally-verified
  • L1 historical tag의 독립 Docker HTTP smoke 수행 — 등급: locally-verified
  • 누적 focused Gradle suite 및 dependency/public-path/env verifier 수행 — 등급: locally-verified
  • 전체 ./gradlew check를 branch 변경과 무관한 base architecture failure 없이 통과 — 등급: needs-confirmation (아래 §검증 기록 참조)

진행 중 메모

  • application repository의 replay branch는 54cf7e7 / nplus1-replay-l12까지 tag가 완료된 상태다.
  • full check의 유일한 base failure는 별도 수정 범위로 남겼다. 이 노트의 evidence grade는 local Docker/Gradle 검증까지만 나타낸다.

결정 사항

  • 2026-07-15 D1: 학습 순서를 원래 구현 시간순이 아니라 L1 → L2 → L3 → L4 → L5 → L6 → L14 → L15 → L16 → Crown → L12의 11개 tag로 고정한다. / 이유: 사용자가 각 commit으로 이동해 API/DB를 직접 관찰해야 한다. / 검토한 대안: 마지막 코드 하나와 문서만 제공. / 근거: 사용자 요구; 구체 tag 순서는 외부 자료가 정하지 않으므로 UNSUPPORTED_IMPL_DECISION.
  • 2026-07-15 D2: 공개 학습 reset은 lab profile의 /api/lab/**에만 두고, anonymous access는 lab:reset 하나에만 허용한다. / 이유: 실제 HTTP 재현은 가능해야 하지만 정상 profile의 feed/권한 정책을 약화하면 안 된다. / 검토한 대안: /api/feed에 reset/debug 파라미터 추가 또는 lab profile 전체 anonymous 허용. / 근거: D4 및 사용자 범위; profile/permission의 구체 모양은 UNSUPPORTED_IMPL_DECISION.
  • 2026-07-15 D3: Crown은 1 native query API, L12는 same-store CQRS-lite 2-query read port API로 병존시킨다. / 이유: 쿼리 수 최소화와 application read-model 분리는 서로 다른 선택지다. / 검토한 대안: L12가 Crown endpoint를 조용히 대체. / 근거: AZURE-CQRS-C2, AZURE-CQRS-C3, SPRING-PROJ-C4.
  • 2026-07-15 D4: Testcontainers 검증에 더해 fresh Docker Compose PostgreSQL에서 HTTP response와 SQL row count를 확인한다. / 이유: test-only assertion으로는 사용자가 직접 API/DB를 따라 보는 목표를 충족하지 못한다. / 검토한 대안: integration test 결과만 보관. / 근거: TC-OFFICIAL-C1, TC-OFFICIAL-C3, TC-OFFICIAL-C5.
  • 2026-07-15 D5: Hibernate addScalar를 Java SQL compile-time checker로 설명하지 않는다. / 이유: 이는 native-query result extraction의 runtime type mapping이며 SQL 문법/컬럼 존재성은 실행 시점에 검증된다. / 검토한 대안: addScalar가 SQL 안전성을 보장한다고 문서화. / 근거: 구현 관찰; UNSUPPORTED_IMPL_DECISION.

결정-근거 매핑

Decision ID Decision 선택 조건 (언제 이 결정 / 언제 대안) Supporting Claims Evidence Strength Open Risk
D1 11개의 checkout 가능한 learning checkpoint 사용자가 단계별 API/DB 관찰을 원할 때; 단일 현재 상태만 필요하면 하나의 branch 상태로 충분 User request; UNSUPPORTED_IMPL_DECISION user-scoped requirement history를 rewrite하면 tag/문서 매핑도 함께 갱신해야 함
D2 lab-only reset/API/anonymous boundary 로컬 학습 profile일 때만; 정상 runtime에서는 lab bean/route 자체를 등록하지 않음 TC-OFFICIAL-C1, TC-OFFICIAL-C3; UNSUPPORTED_IMPL_DECISION official-vendor-doc + unit/HTTP local verification lab profile을 production에 실수로 활성화하지 않는 운영 절차는 별도 확인 필요
D3 Crown 1-query와 L12 2-query CQRS-lite 병존 endpoint-specific optimization을 비교할 때; 별도 store가 필요하면 Full CQRS contract를 별도 설계 raw/official-docs/cqrs-pattern-azure-architecture-center.md#AZURE-CQRS-C2, #AZURE-CQRS-C3, raw/official-docs/spring-data-jpa-projections-spring-official.md#SPRING-PROJ-C4 official-vendor-doc L12은 keyset/visibility를 Crown처럼 모두 포함하지 않음
D4 real PostgreSQL HTTP+DB smoke SQL dialect, container wiring, public response shape를 함께 확인할 때 raw/official-docs/test-taxonomy-testcontainers-official.md#TC-OFFICIAL-C1, #TC-OFFICIAL-C3, #TC-OFFICIAL-C5 official-vendor-doc + local runtime evidence production traffic/permissions을 검증한 것은 아님
D5 addScalar compile-time 보장 부정 native query mapping 설명 시 항상 적용 Local code/runtime behavior; UNSUPPORTED_IMPL_DECISION locally verified implementation fact native SQL의 syntax/plan error는 CI compile이 아니라 query execution에서 발견됨

구현 가이드

1. 고정 replay checkpoint

Trace: D1. 단계 순서와 tag 이름은 사용자 학습 요구에서 정한 UNSUPPORTED_IMPL_DECISION이다. 각 checkpoint의 원래 N+1 원인/해결 설명은 raw/branch-notes/experiment-nplus1-highlight-feed를 참조한다.

순서 Stage Commit Tag checkout 후 주 관찰점
1 L1 e68dd67 nplus1-replay-l1 lazy highlights 컬렉션 N+1
2 L2 138eb67 nplus1-replay-l2 EAGER ToOne fetch 수
3 L3 dc7495c nplus1-replay-l3 two-bag fetch의 예상 실패
4 L4 f257196 nplus1-replay-l4 collection fetch join + paging의 in-memory paging
5 L5 be2a123 nplus1-replay-l5 batch fetch paging
6 L6 cad3c15 nplus1-replay-l6 DTO scalar projection, entity load 0
7 L14 26b196c nplus1-replay-l14 window query로 parent별 Top-N
8 L15 8ff0771 nplus1-replay-l15 keyset cursor
9 L16 3310897 nplus1-replay-l16 viewer visibility + keyset
10 Crown Task 4 3f0b82e nplus1-replay-crown Top-N + keyset + visibility one query
11 L12 54cf7e7 nplus1-replay-l12 CQRS-lite projection port, parent/child two queries

각 stage의 세부 실습은 application repository의 docs/superpowers/plans/*nplus1*lab-guide.md, docs/notes/L*.md, docs/notes/crown.md를 사용한다. L12를 마지막 tag로 둔 것은 원래 번호가 아니라 이 replay의 학습 순서다.

2. lab profile의 API/권한 경계

Trace: D2, D4 / TC-OFFICIAL-C1, TC-OFFICIAL-C3.

  • UNSUPPORTED_IMPL_DECISION: lab profile과 lab:reset permission 이름, marker 소유 방식, anonymous allowlist의 구체 구현은 외부 자료가 정하지 않는다. 정상 profile과 분리된 학습 reset 및 최소 권한 허용이라는 사용자 범위를 우선했다.
항목 replay 계약
profile lab controller/usecase/fixture는 local lab profile에만 등록된다.
normal runtime 기존 advisor를 유지하고 lab usecase/route를 등록하지 않는다. /api/feed의 기존 계약을 바꾸지 않는다.
reset authorization POST /api/lab/feed:reset?count=Nlab:reset을 선언한다. lab profile의 security advisor는 AnonymousAuthenticationToken을 인식해 anonymous에는 lab:reset 하나만 허용하고, 다른 permission은 거부한다. unit+HTTP로 확인했다.
fixture ownership created_by = nplus1-lab marker 데이터만 삭제/재생성한다.
input guard page=10001은 HTTP 400, VALIDATION_FAILED다.

3. Crown과 L12의 의도적 차이

Trace: D3 / AZURE-CQRS-C2, AZURE-CQRS-C3, SPRING-PROJ-C4.

경로 목적 SQL 관찰값 포함 범위
GET /api/lab/feed at Crown/L12 Crown Task 4 최적화 CROWN_TASK4_ONE_QUERY, prepared statement 1, entity load 0 visible parent keyset + Top-3 child를 하나의 native query로 읽음
GET /api/lab/feed/read-model at L12 CQRS-lite read model parent projection 1 + child Top-3 query 1, integration test에서 entity/collection hydration 0 same store의 application query port; Crown을 대체하지 않음

L12의 native child mapping에 사용한 addScalar는 runtime 결과 타입 매핑이다. SQL 문자열의 문법, table/column 이름, plan을 Java compiler가 검증하게 만드는 기능은 아니다. 따라서 native SQL은 Testcontainers/실제 PostgreSQL 실행으로 검증한다.

검증 기록

Docker HTTP + PostgreSQL smoke — final L12 tag

fresh nplus1-final Compose stack에서 nplus1-replay-l12(54cf7e7)을 실행했다.

수행 결과 등급
POST /api/lab/feed:reset?count=100 success=true, feed item 100개, highlight 1,961개 locally-verified
GET /api/lab/feed?size=20&viewer=lab-user-008 item 20개, strategy=CROWN_TASK4_ONE_QUERY, prepared=1, entityLoads=0, parent당 Top-3 최대 3개 locally-verified
GET /api/lab/feed/read-model?page=0&size=20 success=true, item 20개, parent당 Top-3 최대 3개 locally-verified
GET with page=10001 HTTP 400, VALIDATION_FAILED locally-verified
PostgreSQL psql created_by = nplus1-lab row count 100 locally-verified
anonymous authorization AnonymousAuthenticationTokenlab:reset만 허용하고 다른 permission은 거부하는 unit+HTTP 검증 locally-verified

Historical L1 smoke

fresh stack에서 nplus1-replay-l1(e68dd67)을 별도로 실행했다.

수행 결과 등급
reset count=10 success=true locally-verified
feed request item 10개, strategy=L1_LAZY_HIGHLIGHTS, prepared=24, collectionFetch=10 locally-verified

Focused regression suite

다음 filtered cumulative suite는 BUILD SUCCESSFUL, test result의 failures=0, errors=0이었다.

cd /home/donghyeon/workspace/ca-tmpl-nplus1-api/src
./gradlew spotlessApply :application-core:test --tests dev.caskeleton.application.feed.GetFeedReadModelUseCaseTest --tests dev.caskeleton.application.feed.lab.LabFeedUseCaseTest --tests dev.caskeleton.application.feed.lab.LabFeedCursorTest :adapter:inbound:web:test --tests dev.caskeleton.adapter.inbound.web.controller.lab.LabFeedControllerTest :app-bootstrap:test --tests dev.caskeleton.bootstrap.contract.DeveloperExperienceContractTest --tests dev.caskeleton.adapter.outbound.jpa.feed.FeedReadModelUseCaseIT --tests dev.caskeleton.adapter.outbound.jpa.feed.FeedCrownIT --tests dev.caskeleton.adapter.outbound.jpa.feed.FeedVisibilityIT --tests dev.caskeleton.adapter.outbound.jpa.feed.FeedKeysetIT --tests dev.caskeleton.adapter.outbound.jpa.feed.FeedTopNIT --tests dev.caskeleton.adapter.outbound.jpa.feed.FeedProjectionIT --tests dev.caskeleton.adapter.outbound.jpa.feed.FeedBatchFetchIT --tests dev.caskeleton.adapter.outbound.jpa.feed.FeedFetchJoinPagingIT --tests dev.caskeleton.adapter.outbound.jpa.feed.FeedToOneEagerIT --tests dev.caskeleton.adapter.outbound.jpa.feed.FeedMultipleBagIT --tests dev.caskeleton.bootstrap.lab.LabFeedPersistenceIT

명령에 포함된 L1L6, L14L16, Crown, L12의 stage integration test class는 모두 통과했다.

다음 verifier도 통과했다.

./gradlew verifyCleanArchitectureDependencies verifyPublicPathSnapshot verifyEnvKeys

Full check의 기준선 실패

./gradlew check에는 정확히 하나의 잔여 architecture failure가 있었다. 이는 이 replay branch가 수정하지 않은 base commit 6f0b0d6IdempotencyRecordEntity.requestHash에 있는 columnDefinition = "char(64)"가 vendor-neutral entity rule을 위반한 것이다. 따라서 이 노트는 full check를 green이라고 주장하지 않으며, 재현 브랜치의 실패로 귀속하지 않는다.

엣지·실패·의존

  • 실패·엣지 경로: lab profile 밖에서 lab reset을 사용하려 하면 endpoint/usecase가 등록되지 않아야 한다. lab profile에서도 anonymous access는 lab:reset 하나에만 한정되고, 다른 permission은 거부된다.
  • 실패·엣지 경로: native query의 addScalar 타입이 결과와 맞지 않거나 SQL이 잘못되면 compile이 아니라 integration/runtime 실행에서 실패한다.
  • 실패·엣지 경로: L12 read model이 Crown과 동등한 visibility/keyset solution이라고 가정하면 안 된다. L12은 same-store read port의 2-query projection이고 Crown 최적화 endpoint는 유지된다.
  • 다른 계약 의존: raw/branch-notes/experiment-nplus1-highlight-feed의 각 랩 의미와 raw/branch-notes/feature-application-query-bypass-contract의 CQRS-lite/read-port 경계를 소비한다. Full CQRS physical read store는 후자 D2의 escalation 범위다.
  • 검증 환경 의존: DeveloperExperienceContractTest가 root AGENTS.md 존재를 요구해 replay worktree에 일시적인 ignored bridge를 두고 test 후 제거했다. 이는 application commit에 포함되지 않는다.

검증해야 할 주장

Claim Why uncertain How to verify Status
11개 tag가 다른 machine에서도 compose/API guide대로 재현된다 로컬 Docker, Gradle cache, port 상태에 의존 깨끗한 clone/worktree에서 tag별 compose smoke 수행 needs-confirmation
lab profile이 배포 환경에서 활성화되지 않는다 local profile boundary는 production deployment policy를 증명하지 않음 deployment manifest/env registry audit needs-confirmation
IdempotencyRecordEntity failure를 수정한 뒤 full check가 green이 된다 현재 branch가 해당 base failure를 고치지 않음 별도 base-fix branch에서 full check 실행 planned

마주친 문제

  • DeveloperExperienceContractTest가 checkout worktree root의 AGENTS.md를 요구했다.
    • 원인: replay worktree의 contract discovery 조건.
    • 시도: test 실행 중 ignored bridge를 일시적으로 제공.
    • 해결: test 통과 후 bridge를 삭제했고 application history에는 포함하지 않았다.
    • 별도 오류 노트: 아래 Cluster의 raw error 노트.
  • full checkIdempotencyRecordEntity.requestHash vendor-specific columnDefinition에서 멈췄다.
    • 원인: base 6f0b0d6에 이미 존재한 rule violation.
    • 해결: replay scope 밖으로 남기고 base failure로 명시했다.

묶음

아래 raw leaf는 생성되었고, 이 branch를 ## Parent upward link로 가진다. 두 번째 블로그 주제는 별도 raw 문서로 추출하지 않았다.

Sub-branches (세부 작업)

  • 없음 — 11개 checkpoint는 하나의 replay branch history로 관리한다.

오류 기록 (이 branch 작업 중 발생)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

job-posting tie-ins (이 작업에서 파생된 글감)

  • raw/blog-topics/nplus1-lab-checkoutable-api-replay-2026-07-15 — 테스트 코드를 실제 API/DB 학습 환경으로 변환한 방법.
  • raw/blog-topics/crown-query-and-cqrs-lite-boundary-2026-07-15.md — 이번 capture에서는 별도 raw 문서로 추출하지 않았다.
  • derived blog: 생성 전. canonical 검증 후 wiki/blog/ 후보를 결정한다.

관련 일일 노트

  • 해당 없음 — 이 캡처 시점에는 별도 daily-note를 만들지 않았다.

완료 후 정리

  • PR 링크: 없음.
  • 리뷰 메모: 11개 replay tag와 L1/final Docker smoke, focused suite, architecture/public-path/env verifier를 local에서 확인했다.
  • 머지 결과 / 배포 환경: local Docker Compose + local PostgreSQL만 검증. production 배포 검증 없음.
  • wiki 추출 대상 (verified만, wiki/projects/로만 추출):
    • actually-implemented 항목: lab-profile replay API, 11 stage tag catalog, Crown/L12 분리 경로.
    • locally-verified 항목: L1 historical smoke, final Docker HTTP/PostgreSQL smoke, filtered Gradle suite/verifier.
    • prod-verified 항목: 없음.
  • 추출하지 않을 항목 (planned / documented-only / abandoned): base IdempotencyRecordEntity rule failure 해결, production profile/deployment verification.