Files
llm-wiki/raw/branch-notes/chore-ulid-to-uuidv7.md

192 lines
13 KiB
Markdown

---
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) · 사용자 커밋/머지 대기).
<!-- section-id: branch-parent -->
## 부모
- [[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가 이 결정을 참조.
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: ULID format·PostgreSQL uuid persistence·SecureRandom test가 통과한다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| 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]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
기존 branch-local 결정은 아래 `## Decision Evidence Map`의 D-01~D-05가 소유한다. 부모 branch는 legacy packet이라 pinned project decision을 별도로 제공하지 않는다.
<!-- section-id: declared-overrides -->
### 선언한 예외
- 없음.
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
ULID의 시간정렬은 **밀리초 타임스탬프 기준**이라 다중 인스턴스 환경에서 같은 ms 내 순서를 보장하지 못하고, 표준 타입이 아니라 커스텀 라이브러리(`ulid-creator`)+Crockford 코덱+ULID↔UUID 변환 매퍼가 필요했다. **UUIDv7(RFC 9562)** 은 표준 `java.util.UUID`이면서 상위 48비트가 ms 타임스탬프라 ULID와 **동일한 인덱스 지역성**을 유지한다 → 표준화 + 커스텀 의존 제거가 목적(성능 개선이 아님).
<!-- section-id: branch-scope -->
## 범위
### 포함 범위 (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 필드만 `<scrubbed>` 라 영향 없음. `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에서 파생된 자료)
<!-- GENERATED: errors:start -->
- [[raw/errors/gradle-strict-lock-stale-entry-non-resolvable-config-2026-07-08]]
<!-- GENERATED: errors:end -->
### 오류 기록 (이 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 성능 주장.