- analysis/·source-index·state.json 을 final/document.md 제2부·제3부로 접었다. SSOT 는 하나다 - 파일럿 — commit-ambiguity-as-a-result 를 새 기준으로 재선별. 후보 14 → 글감 5 (PROMOTE 5 · MERGE_INTO 3 · KEEP_IN_SSOT 4 · 보류 2). 기록 5건을 다시 썼고 그림 1개를 techviz 로 만들었다 - 재선별이 잡은 것: 제1부 §6.2·§11.1 이 자기 §13.2 와 어긋나 있었다(레인을 안 돌렸다 vs 돌렸다) — 정정. 이미 답이 나와 있던 Question 을 HEAD 재실행 질문으로 다시 세웠다. Concept 이 인용한 코드가 SSOT 에 없어 뺐다 - candidateScope·sourceRepository 기록. 나머지 43개 주제는 재선별 대기(PENDING 905) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
153 lines
12 KiB
Markdown
153 lines
12 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: a05-f012-identity
|
|
title: 상위 클래스까지 올라가는 탐색이 게터는 읽지 않는다
|
|
topic: what-a-gate-does-not-prove
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:a05-f012-identity
|
|
evidenceCapturedOn: 2026-09-02
|
|
body: case-a05-f012-identity.body.md
|
|
assets:
|
|
- key: a05-f012-identity
|
|
file: ../../../final/evidence/rendered/a05-f012-identity.svg
|
|
- key: a05-f012-identity-probe
|
|
file: ../../../final/evidence/rendered/a05-f012-identity-probe.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/a05-f012-identity.txt
|
|
- ../../../final/evidence/raw/a05-f012-identity-probe.txt
|
|
source:
|
|
- 원본 분석 절은 `final/document.md#a05` §38 이다. 탐지 거짓과 검증 통과라는 탐침 결과, 현재 출하 엔티티가 이 결함을 밟는다는 증거가 없다는 판정, 그리고 세 가지 수정 방향이 그 절에 있다. 등급은 P2 이고 이유는 제공자 가드의 정확성과 채택 안전이다.
|
|
- 그 절의 원본 탐침은 한 리비전 앞에서 돌았고 엔티티에 `@Entity` 가 없었다. 여기서는 현재 리비전에서 `@Entity` 를 붙여 다시 돌리고, 하이버네이트 부트스트랩까지 더했다.
|
|
---
|
|
|
|
# 상위 클래스까지 올라가는 탐색이 게터는 읽지 않는다
|
|
|
|
배치가 필요한 프로파일에서 IDENTITY 전략을 거절하는 가드가 선언된 필드만 읽는다. JPA 가 똑같이 허용하는 프로퍼티 접근으로 같은 매핑을 선언하면 통과한다. 하이버네이트는 그 매핑을 필드 접근과 똑같이 읽으므로, 통과한 것은 가드가 막겠다고 쓴 바로 그 동작이다.
|
|
|
|
## 관계
|
|
|
|
- **접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다**
|
|
탐지 방법이 대상의 실제 형태를 못 덮는다는 점이 같다.
|
|
- **@Bean이 있다는 것은 조립 증거가 아니다**
|
|
같은 리프의 확인 규칙이다.
|
|
|
|
## 문제
|
|
|
|
IDENTITY 컬럼의 값은 삽입할 때 데이터베이스가 붙인다. 하이버네이트는 키를 받아야 영속성 컨텍스트에 넣을 수 있어서 삽입을 하나씩 바로 보낸다. hibernate.jdbc.batch_size 값과 무관하다.
|
|
|
|
가드의 javadoc 이 그 조용함을 문제로 지목한다. 설정은 맞아 보이고 임포트는 돌아가며, 증상은 예상보다 열 배쯤 느리다는 것뿐이다. 배치가 필요하다고 선언한 프로파일은 그것을 기동 실패로 바꾼다.
|
|
|
|
## 결론
|
|
|
|
탐지는 @Id 가 붙은 필드를 찾는 것으로 시작한다. 탐색 범위가 상위 클래스까지인 이유는 javadoc 에 적혀 있다. 선언 클래스에서 멈추면 매핑된 상위 클래스가 식별자를 든 흔한 모양을 전부 IDENTITY 아님으로 분류하게 되고, 가드는 자기가 쓰인 이유인 엔티티들을 통과시킨다는 것이다.
|
|
|
|
javadoc 이 든 근거는 프로퍼티 접근에도 그대로 성립한다. 탐색이 도는 것은 getDeclaredFields() 하나다.
|
|
|
|
같은 IDENTITY 매핑을 두 방식으로 선언해 컴파일된 가드에 넘겼다. 필드 접근은 탐지 참, 검증 거절이다. 프로퍼티 접근은 탐지 거짓, 검증 통과다.
|
|
|
|
그 통과가 무해한지 보려고 하이버네이트를 직접 세워 재 봤다. 7.2 메타모델은 앞의 것에 필드를, 뒤의 것에 메서드를 식별자 멤버로 잡는다. 배치 크기를 50 으로 두고 200행을 넣으면 둘 다 프리페어드 스테이트먼트가 200 개다. 같은 조건의 시퀀스 엔티티는 6 개다. 프로퍼티 접근 쪽도 배치가 꺼진다.
|
|
|
|
가드를 부르는 프로덕션 코드는 없다. main 에서 이 타입을 만들거나 임포트하는 파일도, 같은 패키지의 배치 프로파일 레지스트리를 읽는 코드도 없다. 계획이 약속한 산출물은 기동 진단인데, 정작 기동에서 이것을 부르는 자리가 없다.
|
|
|
|
막아야 할 대상 자체가 main 에서 사라졌다. GenerationType.IDENTITY 가 main 에 나오는 자리는 셋인데 전부 가드 자신 안이다. @GeneratedValue 자체가 main 에 없다. 출하되는 엔티티는 식별자를 애플리케이션에서 붙인다.
|
|
|
|
접근 방식으로 갈리는 일도 없다. main 의 @Entity 서른둘 가운데 필드에 @Id 를 단 것이 스물일곱이다. @EmbeddedId 를 쓰는 것이 하나 있다. 게터에 @Id 를 단 곳도, 접근 방식을 뒤집는 @Access 도 없다.
|
|
|
|
남는 것은 계약이다. 이 클래스는 IDENTITY 를 닫힌 방식으로 거절한다고 문서화한 일반 JPA 플랫폼 가드다. 채택자가 프로퍼티 접근을 쓰면 그 문장이 성립하지 않는다.
|
|
|
|
수정 방향은 셋이다. JPA 메타모델로 실제 식별자 속성과 접근 전략을 해석하거나, 필드와 게터를 모두 검사하되 중복과 재정의 규칙까지 JPA 접근 의미와 맞추거나, 지원 매핑을 필드 접근으로 제한하고 그 제한을 아키텍처 규칙으로 강제하는 것이다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21.0.12
|
|
Hibernate ORM : 7.2.24.Final
|
|
확인 방식 : 컴파일된 가드에 두 접근 방식 투입, 하이버네이트 부트스트랩과 문장 계수, 참조와 매핑 계수
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
1. 가드의 탐지 메서드와 그 아래 식별자 탐색 메서드를 읽는다.
|
|
2. 같은 IDENTITY 매핑을 필드 접근과 프로퍼티 접근으로 각각 선언한다.
|
|
3. 배치를 요구하는 프로파일과 함께 컴파일된 가드에 둘 다 넘긴다.
|
|
4. 같은 두 매핑에 시퀀스 엔티티를 더해 하이버네이트를 세우고, 200행을 넣어 프리페어드 스테이트먼트를 센다.
|
|
5. 그 가드를 만들거나 임포트하는 프로덕션 코드를 센다.
|
|
6. main 의 @Entity 와 @GeneratedValue 와 @Access 를 각각 센다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
IDENTITY 컬럼은 값을 데이터베이스가 삽입 시점에 정한다. 하이버네이트는 그 키를 알아야 영속성 컨텍스트에 넣을 수 있으므로 삽입을 하나씩 즉시 실행한다. `hibernate.jdbc.batch_size` 를 얼마로 잡든 마찬가지다.
|
|
|
|
배치가 필요하다고 선언한 프로파일에서 `HibernateBatchConfigurationGuard` 는 그런 엔티티를 거절한다.
|
|
|
|
## 식별자 탐색은 getDeclaredFields() 만 읽는다
|
|
|
|
:::evidence key="a05-f012-identity" alt="가드의 탐지 메서드와 식별자 탐색 메서드 전문, 이 타입을 언급하는 곳 전부와 프로덕션 인스턴스화 수, 같은 패키지 레지스트리의 프로덕션 독자 수, 계획 문서가 이 과제의 산출물로 적은 문장, 가드가 거절하려는 전략이 main 에 나타나는 줄과 main 의 GeneratedValue 수, 그리고 출하 엔티티의 식별자 접근 방식 계수를 출력한 터미널 기록." caption="탐지와 탐색 전문 · 프로덕션 인스턴스화 0 과 레지스트리 독자 0 · 계획서의 기동 진단 · main 의 GeneratedValue 0 · 엔티티 32 중 27이 필드 @Id, 게터 0, @Access 0 — 51줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
탐색이 상위 클래스까지 올라가고, javadoc 이 이유를 적어 뒀다.
|
|
|
|
```text
|
|
stopping at the declared class would silently classify every such entity as "not
|
|
identity" and let the guard pass on exactly the entities it was written for.
|
|
```
|
|
|
|
이 저장소에는 식별자를 든 매핑된 상위 클래스가 없다. main 의 `@MappedSuperclass` 는 하나이고 `@Id` 를 들지 않는다. 그 근거는 채택자를 위해 적힌 것이다.
|
|
|
|
같은 근거가 프로퍼티 접근에도 성립한다. 탐색은 `getDeclaredFields()` 만 순회한다.
|
|
|
|
## 하이버네이트는 게터에 단 것도 IDENTITY 로 읽는다
|
|
|
|
:::evidence key="a05-f012-identity-probe" alt="같은 IDENTITY 매핑을 필드 접근과 프로퍼티 접근으로 선언한 두 엔티티의 소스와 배치를 요구하는 프로파일에서의 탐지·검증 결과, 시퀀스 엔티티를 대조군으로 더해 하이버네이트를 세우고 200행을 넣었을 때의 식별자 멤버와 프리페어드 스테이트먼트 수, 그리고 가드의 근거 측정과 그것이 도는 레인과 워크플로를 출력한 터미널 기록." caption="두 접근 방식의 탐지·검증 결과 · 하이버네이트가 잡은 식별자 멤버 필드와 메서드 · 200삽입에 IDENTITY 200문장 대 시퀀스 6문장 · 근거 측정의 단언과 CI 레인 — 48줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
가드에 넘기면 이렇게 갈린다.
|
|
|
|
```text
|
|
FieldAccessIdentity IDENTITY 탐지 true 검증 거절
|
|
PropertyAccessIdentity IDENTITY 탐지 false 검증 통과
|
|
```
|
|
|
|
통과가 무해한 누락인지 확인하려고 하이버네이트를 직접 세웠다. 배치 크기 50, 200행이다.
|
|
|
|
```text
|
|
필드 접근 식별자 멤버 Field 삽입 200 프리페어드 스테이트먼트 200
|
|
프로퍼티 접근 식별자 멤버 Method 삽입 200 프리페어드 스테이트먼트 200
|
|
시퀀스(대조군) 식별자 멤버 Field 삽입 200 프리페어드 스테이트먼트 6
|
|
```
|
|
|
|
메타모델은 뒤의 것에 메서드를 식별자 멤버로 잡는다. 문장 수는 필드 접근과 같고, 같은 조건의 시퀀스 엔티티와는 서른 배 이상 다르다. 가드가 통과시킨 것은 가드가 막겠다고 쓴 동작이다.
|
|
|
|
거절의 근거도 실측이다. `IdStrategyContractTest` 가 실제 PostgreSQL 에 200행을 배치 크기 50 으로 넣고, 시퀀스 엔티티는 `jdbcBatches` 가 1 보다 크고 IDENTITY 엔티티는 0 이라고 단언한다. 그 태그를 고르는 레인을 PR 워크플로와 야간 워크플로가 둘 다 호출한다. 여기서는 읽기만 했다.
|
|
|
|
## 가드·프로파일·레지스트리는 서로만 참조한다
|
|
|
|
계획 문서는 이 과제의 산출물을 IDENTITY 와 시퀀스 불일치에 대한 기동 진단으로 적었다. 가드 javadoc 과 단위 테스트 javadoc 도 기동 실패를 말한다.
|
|
|
|
main 에 그 기동 지점이 없다. 이 타입을 만들거나 임포트하는 main 파일이 0 이고, 같은 패키지의 배치 프로파일 레지스트리도 main 에서 읽히지 않는다. 이 타입을 언급하는 넷은 자기 자신, 실제로 부르는 단위 테스트 하나, 그리고 javadoc 으로만 부르는 계약 시험과 테스트 도구다.
|
|
|
|
거절 대상도 main 에는 없다. `GenerationType.IDENTITY` 가 main 에 나타나는 세 줄은 가드 자신의 javadoc 과 예외 메시지와 비교식이다. `@GeneratedValue` 는 main 에 한 번도 나오지 않는다. 출하 엔티티의 식별자는 전부 애플리케이션이 정한다.
|
|
|
|
접근 방식도 갈리지 않는다. main 의 `@Entity` 는 서른둘이고 스물일곱이 `@Id` 를 필드에 단다. 하나는 `@EmbeddedId` 를 쓴다. 게터에 `@Id` 를 단 것은 0 이고, 접근 방식을 뒤집는 `@Access` 는 저장소 전체에 0 이다.
|
|
|
|
## 이 가드가 문서화한 보장의 범위
|
|
|
|
채택자가 프로퍼티 접근을 쓰면 이 가드는 그 매핑을 보지 못한다. 하이버네이트는 그것을 IDENTITY 로 부트스트랩하고 배치는 꺼진다.
|
|
|
|
고칠 방향은 셋이다.
|
|
|
|
- JPA 메타모델로 실제 식별자 속성과 접근 전략을 해석한다
|
|
- 필드와 게터를 모두 검사하되 중복과 재정의 규칙까지 JPA 접근 의미와 맞춘다
|
|
- 지원 매핑을 필드 접근으로 제한하고 그 제한을 아키텍처 규칙으로 강제한다
|
|
|
|
게터 리플렉션만 더하면 혼합 접근과 `@Access(AccessType.PROPERTY)` 가 남는다. 이 저장소에는 `@Access` 사용이 0 이라 지금은 드러나지 않는다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
실제 PostgreSQL 에서 재지는 않았다. 부트스트랩 탐침은 H2 위에서 돌렸고, 하이버네이트가 삽입을 배치로 묶었는지는 프리페어드 스테이트먼트 수로 판정했다.
|
|
|
|
<!-- body:end -->
|