--- 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 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.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 버전 컬럼과 만나는 지점의 사례다. ## 본문 상태 전이를 "읽고 → 판단하고 → 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. * *

Every one repeats the complete owner tuple — scope, token, attempt, claim operation and state * revision — in its {@code where} clause, so the update count is 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 /** *

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 은 정상적인 답이고, 그 답을 어떻게 해석할지는 호출자가 정한다. 그래서 경합이 예외 처리 경로가 아니라 정상 경로에 있다. :::