Files
DongHyeonkaandClaude Opus 5 ab59130196 chore: 이전 세션이 남긴 변경을 커밋한다
이번 파이프라인 작업과 무관하게 작업 트리에 남아 있던 것을 그대로 올린다.
사용자가 「전부 커밋」으로 정했고, 이번 작업과 섞이지 않게 커밋만 나눴다.

대부분은 clean-architecture-backend-template 의 그림 정본 재배치다 —
final/assets/diagrams/<이름>/ 에 있던 것이 CLAUDE.md 가 적은 배치인
final/assets/<이름>/ 로 옮겨졌고 .techviz/<이름>/ 이 함께 들어왔다.
삽입 줄의 대부분(3.15M)이 그 .techviz context.json 이다.

그 밖에 ca-tmpl·document-haness 의 정리, .claude/agents/ 열한 개,
writing-practitioner-guides 스킬, .playwright-mcp 세션 산출물,
scripts/check-ssot-facts.py 와 그 시험이 들어 있다.

이 커밋의 내용은 내가 만든 것이 아니라 이전 세션이 남긴 것이고 검증하지 않았다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:02:02 +09:00

128 lines
5.5 KiB
Markdown

---
kind: CONCEPT
slug: cas-tuple-and-update-count
title: CAS 튜플과 update count가 답이 되는 구조
topic: owner-safe-state-machines
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
basisVersion: PostgreSQL 16 · Spring Boot 4.0.8 · 리비전 21234e38
rootTreeNode: concept:cas-tuple-and-update-count
evidenceCapturedOn: 2026-09-01
assets:
- key: cas-tuple-and-update-count
file: ../../../final/evidence/rendered/cas-tuple-and-update-count.svg
- key: cas-tuple-and-update-count-diagram
file: ../../../final/assets/diagrams/cas-tuple-and-update-count/cas-tuple-and-update-count.svg
evidence:
- ../../../final/evidence/raw/cas-tuple-and-update-count.txt
source:
- 원본 분석 절은 final/document.md#4-1 · final/document.md#a05 §10.1, §10.3 이다.
---
# CAS 튜플과 update count가 답이 되는 구조
상태를 전이시키는 모든 문장이 소유자 튜플 전체를 where 절에 반복한다. 그래서 갱신 건수가 곧 답이 된다. 한 건이면 이 소유자가 이 리비전에서 여전히 소유자였다는 뜻이고, 0 이면 다른 무언가가 레코드를 움직였다는 뜻이다.
## 관계
- **fenced lease — 만료 시각만으로는 부족한 이유**
이 구조가 강제하는 소유권 모델이다.
- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다**
이 개념을 규칙으로 옮긴 것이다.
- **native claim이 Version을 올리지 않아 충돌을 보고하지 않는 낙관적 잠금이 됐다**
같은 구조가 JPA 버전 컬럼과 만나는 지점의 사례다.
## 본문
<!-- body:start -->
상태 전이를 "읽고 → 판단하고 → PK로 update"하면 그 사이에 takeover한 worker의 상태를 덮어쓴다. 이 저장소의 형태는 소유권 튜플 전체(scope · owner token · attempt · claim operation id · state revision)를 where 절에 반복하고 **update count 자체를 답으로 쓰는** 것이다.
## 판정이 되는 갱신 행 수
:::evidence key="cas-tuple-and-update-count-diagram" alt="소유권 튜플 조건부 UPDATE 에서 한 행 갱신과 영 행 갱신 두 갈래가 나온다" caption="판정이 되는 갱신 행 수" zoom="false"
:::
1이면 이 owner가 그 revision에서 여전히 owner였고, 0이면 다른 무언가가 record를 움직였으니 caller는 자기 view를 현재로 취급하면 안 된다.
## where 절에 반복되는 소유권 튜플
:::evidence key="cas-tuple-and-update-count" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true"
:::
## polling delivery가 더 얹는 조건
authority `EXISTS` 서브쿼리를 더해 cutover를 가로지르지 못하게 한다.
:::note
컨테이너 레인 미실행 — 동시 claim에서 실제로 0행이 나오는지 관측하지 않았다
:::
## 문장이 답을 만든다
```java
/**
* The statements that advance a claim its owner already holds.
*
* <p>Every one repeats the complete owner tuple — scope, token, attempt, claim operation and state
* revision — in its {@code where} clause, so the update count <em>is</em> the answer: one row means
* this owner was still the owner at this revision, zero means something else moved the record and
* the caller must not treat its own view as current.
*/
```
튜플은 다섯이다. 스코프, 토큰, 시도 번호, 청구 연산, 상태 리비전.
## 읽고 나서 쓰면 안 되는 이유
```java
/**
* Reading the row and then updating on the scope
* alone would let a worker whose lease expired overwrite the state of the one that took over.
*/
```
읽기와 쓰기 사이에 다른 작업자가 들어올 수 있다. 스코프만으로 갱신하면 그 사이의 변화를 보지 못한다.
## 상태 리비전이 함께 오르는 이유
```sql
update idempotency_record
set status = 'EXECUTING',
state_revision = state_revision + 1,
last_transition_operation_id = ?,
last_transition_kind = 'START',
last_transition_result_digest = ?,
```
전이마다 리비전이 오른다. 그래서 같은 소유자라도 자기가 본 리비전이 아니면 갱신이 0 건이 된다. 소유권만으로는 부족하고 시점까지 맞아야 한다.
## 한 자리에 모으는 이유
```java
/**
* <p>Collected here rather than in the store because they are one family: same guard, same
* interpretation of the count, same reason a caller may not skip the guard. The store decides which
* of them a given outcome permits.
*/
```
같은 가드와 같은 해석을 공유하는 문장들을 한 타입에 둔다. 저장소는 어떤 결과에 어떤 전이가 허용되는지만 정한다.
이 분리가 하는 일은 가드를 건너뛰는 문장이 새로 생기지 않게 하는 것이다. 문장이 저장소에 흩어져 있으면 하나가 where 절을 짧게 쓰는 것을 막을 방법이 없다.
## 같은 구조가 다른 곳에도 있다
outbox 폴링 전달 어댑터의 완료 CAS 문장 셋이 같은 형태다. 최종 상태 쓰기가 자기 획득 토큰을 지목하고, 밀려난 작업자의 쓰기는 0 건이 된다.
:::tip
이 구조에서 예외는 실패를 뜻하지 않는다. 갱신 건수 0 은 정상적인 답이고, 그 답을 어떻게 해석할지는 호출자가 정한다. 그래서 경합이 예외 처리 경로가 아니라 정상 경로에 있다.
:::
<!-- body:end -->