Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-assigned-id-turns-claim-into-upsert.md
T
DongHyeonkaandClaude Fable 5.1 b25357c48a docs(clean-architecture-backend-template): fold analysis into final and re-select one topic
- 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>
2026-09-07 12:39:20 +09:00

5.9 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 assigned-id-turns-claim-into-upsert 배정 식별자 때문에 insert-first 청구가 UPSERT 가 되어 커밋된 결과를 덮었다 state-ownership-and-concurrency clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:assigned-id-turns-claim-into-upsert 2026-09-01 case-assigned-id-turns-claim-into-upsert.body.md
key file
assigned-id-turns-claim-into-upsert ../../../final/evidence/rendered/assigned-id-turns-claim-into-upsert.svg
../../../final/evidence/raw/assigned-id-turns-claim-into-upsert.txt
원본 분석 절은 final/document.md#a20-grpc-operation-ledger-jpa#L133 이다.

배정 식별자 때문에 insert-first 청구가 UPSERT 가 되어 커밋된 결과를 덮었다

어댑터가 먼저 넣고 충돌에서 읽는 순서를 자기 존재 이유로 적는다. 엔티티의 식별자가 배정값이라 저장 메서드가 병합으로 가고, 파생 기본 키가 유니크 제약과 같은 행을 가리켜 두 번째 청구가 위반 없이 기존 행을 덮는다.

관계

  • Atomic 타입의 존재는 원자성의 증거가 아니다 같은 계열의 규칙이다.
  • 중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다 테스트 이중과 실제 저장소가 갈리는 지점을 찾는 방법이다.
  • 커밋 증거 프레임을 두 주인이 pop해서 바깥 트랜잭션의 실패가 익명이 됐다 같은 리프 계열의 소유권 사례다.

문제

어댑터의 자바독이 순서를 계약으로 적는다.

먼저 넣고 충돌에서 읽는다는 것이다. 읽고 나서 넣는 구현은 읽기와 넣기 사이에 창이 있고, 그 창의 너비가 정확히 그것이 닫으려는 경합의 너비이며, 두 시도를 동시에 돌리지 않는 모든 테스트를 통과한다는 것이다.

전제는 저장 호출이 삽입이고, 같은 신원의 두 번째 청구가 유니크 제약을 건드린다는 것이다.

결론

두 전제가 모두 성립하지 않는다.

엔티티의 식별자는 호출자가 배정한다. 청구 팩토리가 신원에서 파생한 저장 키를 필드에 채우므로 식별자가 널이 아니다.

Spring Data 의 저장 구현은 식별자가 널이 아닌 엔티티를 새 것으로 보지 않는다. 병합으로 보낸다.

그리고 저장 키는 유니크 제약의 세 컬럼을 이어 붙인 파생값이다. 호출자 지문과 정규 메서드 이름과 멱등 키 해시다.

그러므로 같은 신원의 두 번째 청구는 같은 기본 키 행을 겨냥한다.

실제로 일어나는 일은 이렇다.

병합이 그 행을 찾는다. 유니크 제약은 발화하지 않는다. 새 행을 넣는 것이 아니라 같은 행을 갱신하기 때문이다.

분리 상태의 새 엔티티가 기존 행 위에 복사된다. 상태가 진행 중으로, 결과 참조가 널로, 완료 시각이 널로, 청구 시각이 지금으로 바뀐다.

예외가 없으므로 청구는 빈 값을 돌려준다. 호출자는 자기가 청구를 소유했다고 읽는다.

즉 이미 커밋된 연산의 결과 참조가 지워지고, 재시도가 그 변경을 다시 실행한다. 이 모듈이 존재하는 이유로 든 결과 그대로다.

데이터베이스의 검사 제약도 막지 못한다. 갱신 후 상태는 진행 중이고 완료 시각이 널이라 셋 다 합법이다.

테스트가 이것을 볼 수 없는 이유는 이중에 있다. 인메모리 저장소의 저장 메서드는 키가 이미 있으면 예외를 던진다. 삽입과 유니크 제약을 모사한다.

즉 이중은 삽입을 하고 실제 저장소는 갱신을 한다. 빌드 파일 주석이 실제 데이터스토어를 쓰지 않는 근거로 벤더 의미론이 시험 대상일 때만 정당하다고 적었는데, 여기서 갈린 것이 정확히 벤더 의미론이다.

검증 환경

OpenJDK : 21.0.12 Spring Data JPA : 4.0.7 확인 방식 : 엔티티 식별자 형태와 저장 계약 대조, 파생 키 구성 확인 소스 수정 : x

재현 조건

원문은 document-detail 의 final/document.md#a20-grpc-operation-ledger-jpa 에 있다.

  1. 어댑터의 청구 메서드와 그 자바독을 읽는다.
  2. 엔티티의 식별자가 어디서 채워지는지 확인한다.
  3. 저장 구현이 식별자 널 여부로 무엇을 고르는지 확인한다.
  4. 저장 키의 파생식과 유니크 제약의 컬럼을 대조한다.
  5. 테스트 이중의 저장 메서드가 무엇을 하는지 읽는다.

본문

claim 은 insert-first, read-on-conflict 를 주장한다. 그러나 엔티티의 @Id 가 배정값(caller|method|keyHash)이라 SimpleJpaRepository.savepersist 가 아니라 merge 로 간다.

claim 이 주장하는 순서

:::evidence key="assigned-id-turns-claim-into-upsert" alt="분석 문서 final/document.md#a20-grpc-operation-ledger-jpa 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-operation-ledger-jpa 발췌 — 15줄" zoom="true" :::

위반 대신 갱신이 일어난다

그 파생 키가 유니크 제약의 세 컬럼과 같은 행을 가리키므로 두 번째 청구는 위반을 일으키지 않고 기존 행을 갱신한다 — 상태가 IN_PROGRESS 로, outcome_reference 가 널로 되돌아가고 claim 은 빈 값을 돌려줘 호출자가 소유를 얻었다고 읽는다.

테스트 이중이 그 차이를 가린다

인메모리 테스트 이중의 save 는 키가 있으면 던지므로 INSERT 를 흉내 낸다.

확인하지 못한 것

실제 데이터베이스로 같은 신원을 두 번 청구해 재현하지 않았다. 이 리프는 어떤 배포에도 조립되지 않아 실행 경로가 없다.