Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f001-uuidcodec.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

13 KiB

kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
kind slug title topic project status sourceRevision rootTreeNode evidenceCapturedOn body assets evidence source
CASE a07-f001-uuidcodec UuidCodec 의 메서드를 부르는 줄은 자기 명세 다섯뿐이다 identity-and-identifier clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:a07-f001-uuidcodec 2026-09-02 case-a07-f001-uuidcodec.body.md
key file
a07-f001-uuidcodec ../../../final/evidence/rendered/a07-f001-uuidcodec.svg
key file
a07-f001-uuidcodec-elsewhere ../../../final/evidence/rendered/a07-f001-uuidcodec-elsewhere.svg
../../../final/evidence/raw/a07-f001-uuidcodec.txt
../../../final/evidence/raw/a07-f001-uuidcodec-elsewhere.txt
원본 분석 절은 analysis/07-adapter-outbound-identifier.md#L73 이다. 등급은 P2 다. 세 타입의 리프 밖 소비자 계수, 메서드 호출이 자기 명세뿐이라는 사실, 문자열에서 직접 만드는 경로가 여럿이라는 지적, 컬럼 변환을 하이버네이트가 처리한다는 서술, 그리고 수정 두 가지가 그 절에 있다.
그 절은 직접 만드는 파일이 스무 개 이상이라 적는데, 그 절이 예시로 든 다섯이 모두 프로덕션 소스이므로 같은 정의에서 맞는 수는 열다섯 파일 스물한 줄이다. 소스 세트를 가리지 않고 세면 서른넷이고 그중 열아홉이 테스트다.
애너테이션이 잇는 구간이 UUID 값과 컬럼 사이라는 것, GraphQL 스칼라가 표준 파서보다 좁게 거른다는 것, 그리고 모듈 의존 규칙이 스물한 줄 중 열세 줄을 막는다는 것은 이 기록에서 확인했다.

UuidCodec 의 메서드를 부르는 줄은 자기 명세 다섯뿐이다

이 리프를 아웃바운드 어댑터 모듈 밖에 둔 근거로 UUID 식별자와 코덱 능력이 제시된다. 그 능력을 구현한 타입을 부르는 프로덕션 코드가 없다.

관계

  • @Bean이 있다는 것은 조립 증거가 아니다 타입의 존재와 사용을 나눠 세는 규칙이다.
  • 소비자가 없는 fixture 셋 선언된 타입 수와 프로덕션 호출자 수를 각각 세어 배선되지 않았다는 것을 확인한 다른 리프다.
  • normalize는 canonical이 아닌 입력을 받아 다른 UUID로 조용히 바꾼다 이 타입을 단일 경로로 올릴 때 먼저 고쳐야 하는 것이다.
  • 문서가 선언됐다는 둘은 없고, 선언된 하나는 그 문장에 없다 같은 리프의 문서 정합성 사례다.

문제

모듈 문서가 배치 근거를 한 문장으로 적는다. UUID 식별자와 코덱 능력은 인프라이지 아웃바운드 연동 지점이 아니므로 그 모듈 밖에 둔다는 것이다.

리프의 공개 타입은 셋이다. 가명화기와 업로드 식별자 팩토리와 UUID 코덱이다.

결론

앞의 둘은 리프 밖에서 생성된다. 가명화기는 부트스트랩과 예제 애플리케이션에서, 업로드 식별자 팩토리는 부트스트랩에서다.

셋째는 없다. 그 이름이 리프 밖에 나오는 자리는 넷인데 둘은 몽고 테스트킷이 임포트하는 드라이버의 동명 타입이고 하나는 그 드라이버 설정을 적은 ADR 문서다. 이 리프의 타입을 가리키는 것은 하나뿐이고, 그것은 호출이 아니라 예제 애플리케이션 README 의 산문이다. 그 산문은 코덱을 쓰지 않는 이유를 적는다.

메서드를 부르는 줄은 저장소 전체에서 다섯이고 다섯 다 자기 명세다.

리프 밖 프로덕션에서 문자열을 UUID 로 바꾸는 자리를 세면 파일 열다섯에 줄 스물하나다. 그 열다섯이 하는 일이 다 같지는 않다. GraphQL 의 UUID 스칼라는 표준 파서가 관대하다는 것을 자바독에 적고 정규형 정규식으로 먼저 거른 뒤에야 파서를 부른다. 코덱으로 갈아 끼우면 검사 폭이 줄어든다.

문서가 코덱의 다른 목적으로 든 컬럼 변환은 하이버네이트의 타입 코드 애너테이션이 맡는다. 다만 그 애너테이션이 붙은 서른여덟 자리의 필드 타입은 전부 UUID 다. 애너테이션이 잇는 것은 UUID 값과 컬럼 사이이고, 문자열과 UUID 사이는 여전히 손으로 짜여 있다. 예제의 영속 매퍼가 그 두 겹을 한 파일에서 보여 준다.

이 분산은 배치 규칙의 결과다. 모듈 레지스트리에서 이 리프에 의존해도 되는 모듈은 예순둘 중 둘, 부트스트랩과 예제 애플리케이션뿐이다. 그중 열세 줄은 규칙을 손대기 전에는 그 타입에 닿을 수 없는 자리다. 남는 여덟 줄 중 둘은 예제 README 가 쓰지 않는 이유를 이미 적어 두었다.

코드 자체에는 결함이 없다. 서른 줄짜리 유틸이고 자기 시험은 다 통과한다. 어긋난 것은 논거다.

그래서 선택지는 좁다. 능력을 논거에서 지우거나 배치 규칙을 손보는 것뿐이다. 그 타입을 실제 단일 경로로 올리려면 규칙을 먼저 바꿔야 하고, 그 다음에는 같은 문서 다음 절이 다루는 정규화 문제를 고쳐야 한다.

이 기록이 세는 것은 소비자의 유무이지 단일 경로 여부가 아니다. 단일 경로를 물으면 앞의 둘도 통과하지 못한다. 리프 밖 프로덕션 스물다섯 파일이 표준 라이브러리 생성기를 직접 부른다.

검증 환경

확인 방식 : 저장소 전수 참조 계수, 모듈 의존 레지스트리 조회 소스 수정 : x

재현 조건

  1. 모듈 문서가 적은 배치 근거를 읽는다.
  2. 리프의 공개 타입을 나열한다.
  3. 각 타입 이름이 리프 밖에 나오는 곳을 확장자 제한 없이 전수로 센다.
  4. 그중 이 리프의 타입을 가리키는 것과 동명의 다른 타입을 가른다.
  5. 그 타입의 메서드를 부르는 줄을 저장소 전체에서 센다.
  6. 문자열에서 UUID 를 직접 만드는 리프 밖 프로덕션 파일과 줄을 전부 나열한다.
  7. 그중 표준 파서를 그대로 쓰지 않는 것이 있는지 확인한다.
  8. 컬럼 변환 애너테이션이 붙은 필드의 타입을 센다.
  9. 레지스트리에서 이 리프의 소비자 후보를 센다.

본문

모듈 문서가 이 리프를 아웃바운드 어댑터 모듈 밖에 둔 이유를 세 줄로 적는다.

논거와 공개 타입

:::evidence key="a07-f001-uuidcodec" alt="모듈 문서가 적은 배치 근거 세 줄과 리프의 공개 타입 셋. 각 타입 이름이 리프 밖에 나오는 곳을 확장자 제한 없이 전수로 검색한 결과. 그리고 UUID 코덱의 메서드를 부르는 줄을 저장소 전체에서 검색한 결과를 출력한 터미널 기록." caption="가명화기는 리프 밖 생성 둘, 업로드 식별자 팩토리는 하나 · 코덱 이름이 나오는 넷 중 둘은 드라이버의 동명 타입, 하나는 ADR 문서, 하나는 예제 README 의 산문 · 메서드 호출은 자기 명세 다섯 줄 — 40줄 · exit 0" zoom="true" :::

- Kept out of `adapter-outbound` on purpose: a UUID id/codec capability is
  infrastructure, not an outbound integration point, so `adapter-outbound` keeps its
  documented meaning (external HTTP / messaging / cache / notifications).

능력은 하나로 적혀 있고, 공개 타입은 셋이다. 셋 중 둘은 리프 밖에서 생성된다 — 가명화기는 부트스트랩과 예제 애플리케이션에서, 업로드 식별자 팩토리는 부트스트랩에서.

이름이 겹친 참조들

코덱 이름이 리프 밖에 나오는 자리는 넷이다.

    docs/adr/ADR-MONGO-002-bson-representation.md:44:pins `UuidCodec(STANDARD)` explicitly …
    src/sample-portfolio/README.md:333:- … `UuidCodec` 같은 공용
    src/adapter/outbound/persistence-mongo/src/testkit/.../MongoBsonSnapshot.java:16:import org.bson.codecs.UuidCodec;
    src/adapter/outbound/persistence-mongo/src/testkit/.../MongoBsonSnapshot.java:59: … new UuidCodec(UuidRepresentation.STANDARD)),

뒤 둘은 몽고 드라이버의 동명 타입이고, ADR 은 그 드라이버 설정을 적은 문서다. 이 리프의 타입을 가리키는 것은 예제 README 한 줄뿐인데, 그 줄은 호출이 아니라 쓰지 않는 이유를 적는 산문이다.

저장소 전체에서 그 타입의 메서드를 부르는 줄은 다섯이고, 다섯 다 자기 명세 파일이다.

같은 변환을 하는 다른 자리들

:::evidence key="a07-f001-uuidcodec-elsewhere" alt="문자열에서 UUID 를 직접 만드는 리프 밖 프로덕션 파일 수와 호출 줄 수와 그 파일 전부의 목록. 그중 GraphQL 스칼라가 표준 파서의 관대함을 적고 정규형 정규식으로 먼저 거르는 구간. 컬럼 변환 애너테이션을 단 프로덕션 파일 수와 애너테이션 수와 그 애너테이션이 붙은 필드의 타입 분포. 예제의 영속 매퍼가 문자열 구간을 따로 처리하는 줄. 그리고 모듈 레지스트리에서 이 리프에 의존해도 되는 모듈 수와 예제 README 가 코덱을 쓰지 않는 이유를 적은 세 줄을 출력한 터미널 기록." caption="직접 만드는 곳은 열다섯 파일 스물한 줄 · GraphQL 스칼라는 정규형 정규식으로 먼저 거름 · 컬럼 변환 애너테이션은 스물세 파일 서른여덟 개이고 붙은 필드는 전부 UUID · 리프에 의존해도 되는 모듈은 예순둘 중 둘 — 43줄 · exit 0" zoom="true" :::

문자열에서 UUID 를 만드는 일은 리프 밖 프로덕션 열다섯 파일에서 스물한 줄이 한다. 애플리케이션 코어의 파일·업로드 식별자, 세 메시징 리프의 매퍼, GraphQL 의 UUID 스칼라, 몽고의 커서 코덱, 알림의 라우팅 계획 코덱, JPA 의 멱등 청구 저장소, 예제의 웹 컨트롤러 넷과 영속 매퍼 둘이다.

열다섯이 모두 같은 일을 하지는 않는다.

 * <p>{@link UUID#fromString} is lenient  it happily accepts {@code "1-1-1-1-1"}  so accepting
 * whatever it parses would make the wire contract depend on a JDK quirk and let two different
 * strings denote the same identifier. The canonical form is enforced explicitly instead.

GraphQL 스칼라는 표준 파서의 관대함을 알고 정규형 정규식으로 먼저 거른다. 이쪽을 코덱으로 바꾸면 검사가 느슨해진다.

컬럼 변환은 다른 구간을 잇는다

#   @JdbcTypeCode(SqlTypes.UUID) 를 단 프로덕션 파일 : 23
#   그 애너테이션 개수 : 38
#   그 애너테이션이 붙은 필드의 타입 : {'UUID': 38}

애너테이션이 잇는 것은 UUID 값과 PostgreSQL 컬럼 사이다. 문서가 toUuid/fromUuid 의 목적으로 적은 두 구간 중 문자열과 UUID 사이는 여기에 없고, 위의 스물한 줄이 각자 처리한다. 예제의 영속 매퍼가 두 겹을 한 파일에서 보여 준다 — 엔티티는 애너테이션으로 컬럼을 잇고, 매퍼가 표준 파서로 문자열을 잇는다.

public static UUID toUuid(WorkLogId id) {
  return UUID.fromString(id.value());

이 분산은 배치 규칙의 결과다

#   전체 62 중 2 : app-bootstrap, sample-portfolio

모듈 레지스트리에서 이 리프에 의존해도 되는 모듈은 둘뿐이다. 스물한 줄 중 열세 줄은 규칙을 먼저 바꾸지 않으면 그 타입을 부를 수 없다. 남는 여덟 줄 중 둘에 대해서는 예제 README 가 이유를 적어 두었다.

- 36자 canonical UUID 와 PostgreSQL native `uuid`(128비트)를 서로 변환합니다. `UuidCodec` 같은 공용
  코덱이 아니라 JDK `java.util.UUID` 를 **직접** 쓰는 이유: 영속 어댑터는 경계 규칙상
  `adapter-outbound`(코덱이 있는 곳)에 의존하면 안 되기 때문입니다(stdlib 이라 의존 문제 자체가 없음).

모듈을 그 자리에 둔 규칙이 그 모듈의 능력을 부를 수 없게 만든다.

남는 선택지

이 타입은 서른 줄짜리 유틸이고 자기 명세를 통과한다. 논거에서 그 능력을 빼거나, 배치 규칙을 다시 여는 것이 남는다. 단일 경로로 올리는 쪽은 규칙 변경이 선행이고, 그 다음에는 같은 문서 다음 절이 다루는 정규화 문제가 온다.

이 기록이 센 것은 소비자의 유무다. 단일 경로 여부를 물으면 앞의 두 타입도 통과하지 못한다 — 리프 밖 프로덕션 스물다섯 파일이 표준 라이브러리 생성기를 직접 부른다.

확인하지 못한 것

스물한 줄 중 열세 줄은 모듈 의존 규칙상 이 타입을 부를 수 없어 동작 비교를 물을 단계가 아니다. 규칙이 허용하는 여덟 줄에 대해서만 대체 시 동작이 같은지 확인하지 않았다.