--- title: branch / chore-ulid-to-uuidv7 (ID 생성 전략 ULID → UUIDv7) source_type: branch-note status: raw id: BR-CA-SKELETON-CHILD-F1674A3D kind: branch-child project: ca-skeleton-operational-contract work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-046 inherits: - DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-DATABASE-001@1 - DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-RANDOM-001@1 refines: [] overrides: [] depends_on: [] contract_packet: 1 branch: chore-ulid-to-uuidv7 parent_branch: feature-resource-identifier-contract git_branch: refactor/ulid-to-uuidv7 related_projects: [ca-skeleton, nplus1-presentation-prep] tags: [branch, ca-skeleton, data-modeling, persistence, api-design, ulid, uuid-v7] created: 2026-07-08 target_merge: develop status_label: in-progress contract_packet_sha256: 59f232e47d66c49ed76c4e3ffa50cab20877fe73c40037b9e3d49d1b9707b984 --- # branch: chore-ulid-to-uuidv7 — ID 생성 전략 ULID → UUIDv7 > Layer: `raw/branch-notes/` — ca-tmpl의 엔티티 식별자 생성을 ULID에서 **UUIDv7**로 교체. [[raw/branch-notes/feature-resource-identifier-contract]](D3/D5/D10/D17)를 개정하는 계약-레벨 변경. > 실제 git 브랜치: `refactor/ulid-to-uuidv7`. 위키 파일명은 prefix 규칙상 `chore-`. > ADR: `ca-tmpl:docs/choice/0001-id-strategy-ulid-to-uuidv7.md`. > `status_label`: `in-progress` (구현 완료 · `./gradlew check` 전량 GREEN 로컬 검증됨 (2026-07-08) · 사용자 커밋/머지 대기). ## 부모 - [[raw/branch-notes/feature-resource-identifier-contract]] — D3 canonical form, D5 no-direct-gen, D10 ULID↔uuid 저장, D17 no-long-PK를 소유하며 이 브랜치가 D3/D10을 UUIDv7 기준으로 정제한다. - 트리거: [[raw/branch-notes/experiment-nplus1-highlight-feed]] — N+1 피드 도메인 파운데이션 가이드 작성 중 ID 규약(ULID) 재검토에서 파생. 그쪽 §0.2가 이 결정을 참조. ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: ULID format·PostgreSQL uuid persistence·SecureRandom test가 통과한다 ### 상속한 프로젝트 결정 | Decision Ref | Project Summary | Branch Application | Source | |---|---|---|---| | `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-DATABASE-001@1` | database는 PostgreSQL 16 단일 stack이다 | DB 컬럼을 native `uuid`로 유지하고 문자열/생성 전략만 UUIDv7로 바꾼다. | [[raw/project-notes/ca-skeleton-operational-contract]] | | `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-RANDOM-001@1` | ULID·idempotency key·token 생성의 random source는 SecureRandom이다 | UUIDv7 generator와 금지 rule이 project random-source 경계를 유지하게 한다. | [[raw/project-notes/ca-skeleton-operational-contract]] | ### 브랜치 지역 결정 기존 branch-local 결정은 아래 `## Decision Evidence Map`의 D-01~D-05가 소유한다. 부모 branch는 legacy packet이라 pinned project decision을 별도로 제공하지 않는다. ### 선언한 예외 - 없음. ## 목표 ULID의 시간정렬은 **밀리초 타임스탬프 기준**이라 다중 인스턴스 환경에서 같은 ms 내 순서를 보장하지 못하고, 표준 타입이 아니라 커스텀 라이브러리(`ulid-creator`)+Crockford 코덱+ULID↔UUID 변환 매퍼가 필요했다. **UUIDv7(RFC 9562)** 은 표준 `java.util.UUID`이면서 상위 48비트가 ms 타임스탬프라 ULID와 **동일한 인덱스 지역성**을 유지한다 → 표준화 + 커스텀 의존 제거가 목적(성능 개선이 아님). ## 범위 ### 포함 범위 (69파일) - **생성기**: `com.github.f4b6a3:ulid-creator:5.2.3` → `com.github.f4b6a3:uuid-creator:6.1.1`, `UlidCreator.getMonotonicUlid()` → `UuidCreator.getTimeOrderedEpochPlus1()`(UUIDv7, **모노토닉 변형** — 구 `getMonotonicUlid()` 의 밀리초-내 단조증가 의도를 보존; jar javap 로 API 확인). build.gradle 2곳 + 락파일 재생성(app-bootstrap `sampleFixture` config 는 `canBeResolved=false` 라 `resolveAndLockAll` 이 못 만져서 stale `ulid-creator` 줄을 수동 병합 제거). - **코덱/팩토리 리네임**: `UlidCodec`→`UuidCodec`(+ Spock spec), `UlidPosterIdFactory`/`UlidWorkLogIdFactory`/`UlidOutboxEventIdFactory` → `Uuid*`. - **도메인 ID 값객체**: 정규식 26자 Crockford ULID → 36자 표준 UUID. `PosterId`/`WorkLogId` javadoc 갱신. - **매퍼**: `Ulid.from(id).toUuid()` → `UUID.fromString(id.value())`, `Ulid.from(uuid).toString()` → `uuid.toString()` (변환 소멸, `java.util.UUID` stdlib). - **웹**: 컨트롤러 `toId`, ID 시리얼라이저 — ULID 대문자 정규화 → UUID 소문자 canonical. - **ArchUnit**: `NO_UUID_RANDOM_IN_CONTROLLER`의 FQN `com.github.f4b6a3.ulid.UlidCreator` → `com.github.f4b6a3.uuid.UuidCreator`(`UUID.randomUUID` 금지는 유지). `NO_LONG_ID_PK` 주석(D10) 갱신. - **문서**: identifier·sample-portfolio CLAUDE.md/README, `ContractSnapshots`, 테스트 19개(픽스처 26자→36자). - **불변**: DB `uuid` 컬럼(128비트) 그대로 — 상위 비트만 v7 레이아웃. ### 제외 범위 - PostgreSQL column type 변경과 data migration은 수행하지 않는다. - local test 결과를 prod 성능 또는 다중 인스턴스 순서 보장으로 승격하지 않는다. ## 근거 (필수, 최소 1개+) | Source | 정당화하는 결정 | |---|---| | [[raw/official-docs/rfc9562-uuid]] | D-01의 UUIDv7 layout과 timestamp-ordered identifier 정의를 뒷받침한다. | | [[raw/official-docs/ulid-spec]] | 기존 ULID format·monotonic semantics와 UUIDv7 전환 전후 경계를 비교한다. | ## TODO - [x] UUIDv7 generator·codec·factory·mapper·fixture 전환 — 등급: `actually-implemented` - [x] `./gradlew check`와 monotonic 1000-loop 검증 — 등급: `locally-verified` - [ ] 부모 identifier 계약 D3/D10과 project WI-046 completion text 갱신 — 등급: `planned` - [ ] 사용자 commit·merge와 downstream 소비자 확인 — 등급: `needs-confirmation` ## 진행 중 메모 - 구현과 local 검증은 끝났지만 부모 계약과 project registry는 아직 ULID 문구를 소유한다. - N+1 branch는 변경 계기만 제공하며 이 branch의 project owner나 work-item dependency가 아니다. ## 결정 사항 - 2026-07-08 D-01: resource identifier canonical form을 ULID에서 UUIDv7로 바꾼다. / 이유: native `UUID` wire/storage shape와 generator 표준화를 맞춘다. / 대안: ULID 유지, UUIDv4. / 근거: [[raw/official-docs/rfc9562-uuid]], [[raw/official-docs/ulid-spec]]. - 2026-07-08 D-03: UUIDv7 generator는 millisecond 내 단조 증가 의도를 보존하는 `getTimeOrderedEpochPlus1()`을 사용한다. / 검증: local API inspection과 loop test. - 2026-07-08 D-05: PostgreSQL native `uuid` column은 유지하고 변환 mapper만 제거한다. / 근거: inherited database decision. ## 결정-근거 매핑 | Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk | |---|---|---|---|---| | D-01 | ULID → UUIDv7 채택 | ADR docs/choice/0001; 사용자 결정(다중 인스턴스 순서 한계 + 비표준). UUIDv7 상위 48비트 ms = ULID와 동일 지역성 | Strong | API 브레이킹(26→36자) — 다운스트림/스냅샷 갱신 필요 | | D-02 | UUIDv4는 기각 | 랜덤 PK = B-tree 단편화(페이지분할·캐시지역성↓), 쓰기多 테이블 성능 후퇴 | Strong | 없음(발표 시연 소재로 별도 활용) | | D-03 | 생성 = uuid-creator `getTimeOrderedEpochPlus1()` (모노토닉 UUIDv7) | jar `javap` 로 메서드 시그니처 확인 + `check` GREEN. `getTimeOrderedEpoch()`(비-모노토닉) 대신 Plus1 선택 = 구 `getMonotonicUlid()` 의도(ms-내 단조증가) 미러 | Strong (검증됨) | 없음 — 모노토닉 문자열 정렬 1000-loop 테스트 GREEN | | D-04 | ArchUnit FQN ulid→uuid 교체(생성 위치 강제 유지) | CleanArchitectureTest `NO_UUID_RANDOM_IN_CONTROLLER` 수정 + suite GREEN | Strong (검증됨) | 없음 | | D-05 | 저장 스키마 불변(native uuid) | 매퍼만 변환 제거, 마이그레이션 무수정 | Strong | 없음 | ## 구현 가이드 | Anchor | 적용 | 검증 | |---|---|---| | identifier adapter | `UuidCreator.getTimeOrderedEpochPlus1()`으로 ID를 생성하고 domain은 factory port만 사용한다. | monotonic 1000-loop와 adapter test | | web/serialization | UUID canonical lowercase 36자를 입력·출력 계약으로 사용한다. | wire test와 snapshot scrubber | | persistence | `UUID.fromString`/`UUID.toString`을 사용하고 native `uuid` column을 유지한다. | mapper/integration test와 migration diff 없음 확인 | | architecture rule | controller direct generation 금지를 `UuidCreator` FQN 기준으로 유지한다. | `CleanArchitectureTest` | ## 엣지·실패·의존 - **실패·엣지 경로**: 26자 ULID consumer가 남아 있으면 path parsing과 snapshot contract가 깨진다. downstream fixture와 wire contract를 함께 갱신한다. - **실패·엣지 경로**: native `uuid` column까지 변경하면 불필요한 data migration이 생긴다. schema는 유지한다. - **다른 계약 의존**: [[raw/branch-notes/feature-resource-identifier-contract]]의 D3·D5·D10·D17과 project `WI-CA-SKELETON-OPERATIONAL-CONTRACT-046`을 소비한다. ## 검증해야 할 주장 **리팩터 GREEN 검증 완료 (2026-07-08, `./gradlew check` BUILD SUCCESSFUL 4m31s, 200 tasks).** 아래는 확정 결과: 1. ✅ `./gradlew check` 전량 GREEN(spotless/checkstyle/spotbugs/errorprone/ArchUnit/`verifyDependencyLocks`/`verifyPublicPathSnapshot`/`verifyCleanArchitectureDependencies`/all tests + Testcontainers 통합 + sampleOffTest). `locally-verified`. 2. ✅ `uuid-creator:6.1.1` resolve + 락 재생성(`resolveAndLockAll --write-locks`) 성공. UUIDv7 메서드 = `getTimeOrderedEpochPlus1()`(jar javap 확인, 모노토닉). `locally-verified`. 3. ✅ 테스트 픽스처(26자 ULID → 36자 canonical UUID `0190bd6e-7c3e-7abc-8def-0123456789ab` 등) 전부 갱신 + 의미 보존: `WorkLogIdTest.rejectsCrockfordUlidFormat` 는 구 26자 형식이 **이제 거부됨**을 증명하는 회귀가드로 신설. 모노토닉 1000-loop 문자열 정렬 테스트 GREEN. property 테스트(jqwik)는 canonical UUID 생성기로 재작성. `locally-verified`. 4. ✅ API 브레이킹(ID 문자열 26→36자) — 와이어 테스트 `.value(ID)` 새 UUID로 일치, `ContractSnapshots` 스크러버 정규식 ULID→UUID 로 교체(엔티티 id 가 스냅샷에 새면 계속 스크럽됨). 커밋된 `.approved.txt` 스냅샷은 volatile 필드만 `` 라 영향 없음. `locally-verified`. 5. ⏳ **canonical 계약 개정 미완(후속)**: [[raw/branch-notes/feature-resource-identifier-contract]] D3(canonical form)·D10(저장 변환) 결정 텍스트를 wiki/registries에서 UUIDv7로 갱신 필요. `planned`. ## 다음 단계 1. ✅ 에이전트 구현 완료 → `check` 전량 GREEN(69파일 변경: 57 M · 6 D · 6 새파일(?? Uuid* 리네임 대상)) 검증됨(2026-07-08). 2. feed 파운데이션 가이드 §0.2/§2.1/§4.4를 최종 UUIDv7 패턴(`getTimeOrderedEpochPlus1`)으로 갱신. 3. 사용자 커밋 → develop 머지 → `lab/nplus1-highlight-feed` 반영 → Task 0. 4. [[raw/branch-notes/feature-resource-identifier-contract]] canonical(D3/D10) 개정(후속). ## 마주친 문제 - Gradle strict lock 갱신이 non-resolvable `sampleFixture`의 stale entry를 제거하지 못했다. - 해결과 재현 근거: [[raw/errors/gradle-strict-lock-stale-entry-non-resolvable-config-2026-07-08]]. ## 묶음 (이 branch에서 파생된 자료) - [[raw/errors/gradle-strict-lock-stale-entry-non-resolvable-config-2026-07-08]] ### 오류 기록 (이 branch 작업 중 발생) - [[raw/errors/gradle-strict-lock-stale-entry-non-resolvable-config-2026-07-08]] — `resolveAndLockAll` 이 `canBeResolved=false` 인 `sampleFixture` config 를 건너뛰어 app-bootstrap lockfile 에 stale `ulid-creator` 줄이 남은 문제 + 수동 병합 해결. ### 면접 준비 - 새 질문 없음 — 식별자 생성/거버넌스 면접 소재는 기존 [[raw/interviews/clean-architecture-identifier-generation]] 이 이미 커버(UUIDv7 vs ULID 인덱스 지역성 각도는 그 노트 갱신 시 반영). ### 블로그·채용공고 연계 글감 - 별도 신규 글감 없음 — ULID→UUIDv7 표준화·인덱스 지역성 각도는 이 branch note 자체가 entry point 이며 기존 [[raw/blog-topics/ulid-crockford-base32-excluded-letters-2026-06-01]] 클러스터에 속함. ## 관련 일일 노트 - 연결된 daily-note 없음. ## 완료 후 정리 - PR 링크: 없음 — 사용자 commit 대기. - 리뷰 메모: local full check와 identifier-specific regression은 통과했고 부모 계약 갱신은 남아 있다. - 머지 결과 / 배포 환경: local verification만 완료, staging/prod 검증 없음. - **wiki 추출 대상**: UUIDv7 generator·wire canonical form·native `uuid` persistence 유지의 locally-verified 결과. - **추출하지 않을 항목**: parent canonical/registry 갱신 전 project-wide 완료 주장과 prod 성능 주장.