docs(clean-architecture-backend-template): 제1부가 채택한 것만 글감으로 남기고 다시 고른다

글감 1,001개 중 제1부(§3~§11) 앵커를 하나라도 가진 것은 112개뿐이었다. 나머지 889개는
제2부 모듈 분석 65편의 절 제목에서 나온 것이고, 그것이 재판정이 필요했던 이유다.

  주제      44 → 16   (43개가 독자 질문 없이 있었다. 지금은 전부 있다)
  글감   1,001 → 123  (제1부 앵커 112 + 제1부가 채택했는데 비어 있던 자리 11)
  후보      965 → 1,088 · PENDING 905 → 0
  error   3,042 → 0

내려온 889개는 후보 대장에 KEEP_IN_SSOT 로 남는다 — 버린 것이 아니라 분석에 남기고 독립
기록으로 만들지 않기로 한 것이다. 그 글감을 받치던 기록 파일 828개는 지웠다. 계약이 정본이고,
파일이 남아 있다는 이유로 계약에서 뺀 주제가 되살아나면 안 된다. 이력에는 그대로 있다 —
git checkout a0ca2bb -- <경로>.

제1부가 채택했는데 글감이 없던 자리 열하나를 채웠다: mongo high-water mark 가 재전달 이벤트를
삼킨 P1, admin plane 이 가드만 켜고 서비스는 켜지 않은 것과 그 짝인 결정, 실패 어휘 세 층과
SQLState 매트릭스 병합 규칙, 부하 아래에서만 새는 admission 경계, 발행 증거와 완료 판정의
분리, keyset·JSONB 결정 둘.

Concept 17개에 basis-version 을 채우고, 계약 제목과 기록 제목이 갈라져 있던 23건을 기록 쪽에
맞췄다. candidateScope 에 excludedAnchorPattern 을 적어 제2부 앵커만 가진 글감이 다시 올라올
수 없게 한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-07 15:02:25 +09:00
co-authored by Claude Opus 5
parent a0ca2bb72a
commit 1f04117bbf
851 changed files with 5498 additions and 90638 deletions
@@ -1,111 +0,0 @@
---
kind: CASE
slug: a-cleanup-claim-without-fencing
title: claim이 소유자·토큰·만료를 기록하지 않아 죽은 worker의 항목이 영영 남았다
topic: fileserver-state-and-fencing
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:a-cleanup-claim-without-fencing
evidenceCapturedOn: 2026-09-02
assets:
- key: a-cleanup-claim-without-fencing
file: ../../../final/evidence/rendered/a-cleanup-claim-without-fencing.svg
evidence:
- ../../../final/evidence/raw/a-cleanup-claim-without-fencing.txt
source:
- 컬럼 정의와 두 결과의 사후 기록은 `V3__fileserver_fenced_cleanup_lease.sql` 의 헤더와 두 COMMENT 에 있다. 토큰 대조와 널 만료 제외의 이유는 `FileserverCleanupRepository` 의 두 메서드 javadoc 에 있다. 원본 분석은 `final/document.md#a05` §79 가 이 스키마를 다룬다.
---
# claim이 소유자·토큰·만료를 기록하지 않아 죽은 worker의 항목이 영영 남았다
정리 항목의 청구가 상태만 바꾸고 소유자도 토큰도 리스 만료도 기록하지 않았다. 하나의 누락에서 회수되지 않는 항목과 덮어쓰기라는 두 결과가 나왔다.
## 관계
- **fenced lease — 만료 시각만으로는 부족한 이유**
이 사례가 같은 문제의 파일서버 판이다.
- **리스가 만료 시각만 기록하고 소유자를 기록하지 않아 최종 상태를 되돌릴 수 있었다**
같은 형태가 메시징 어댑터에서 나타난 사례다.
- **cleanup이 읽은 lease와 삭제 사이에 writer가 그 lease를 얻을 수 있었다**
같은 저장소의 다음 마이그레이션이 다룬 문제다.
## 문제
정리 작업은 파일을 물리적으로 지운 뒤 데이터베이스를 정산한다. 그 사이에 작업자가 죽을 수 있고, 여러 작업자가 같은 항목을 두고 겹칠 수 있다.
청구가 그 두 상황을 구별할 정보를 남기지 않았다. 상태를 진행 중으로 옮기는 것이 전부였다.
## 결론
수정은 컬럼 넷과 질의 둘이다.
컬럼은 청구 소유자, 청구 토큰, 리스 만료, 그리고 청구 세대 계수기다. 질의는 완료 갱신이 토큰으로 행을 찾게 만든 것과, 회수기가 만료된 청구를 오래된 것부터 가져오되 널 만료는 제외하게 만든 것이다.
두 결과가 어떻게 생겼고 각 컬럼이 무엇을 맡는지는 본문이 다룬다.
## 검증 환경
데이터베이스 : PostgreSQL
마이그레이션 도구 : Flyway
확인 방식 : V3 마이그레이션의 컬럼 정의와 정리 저장소 질의 두 개 확인, claim_fence 참조 전수 검색
소스 수정 : x
## 재현 조건
1. fileserver 의 V3 마이그레이션 헤더를 읽는다. 두 결과가 나란히 적혀 있다.
2. 추가된 컬럼 넷과 각각의 널 허용 여부를 확인한다.
3. 완료 갱신 질의가 무엇으로 행을 찾는지 확인한다.
4. 회수기 질의가 널 만료를 어떻게 다루는지 확인한다.
5. claim_fence 를 읽는 코드가 있는지 검색한다.
## 본문
<!-- body:start -->
정리 항목의 청구가 상태를 `IN_PROGRESS` 로 옮기고 그 외에는 아무것도 기록하지 않았다. 마이그레이션 헤더가 그 누락에서 나온 결과를 둘로 적는다.
## 하나는 아무도 손대지 않아서, 하나는 두 손이 겹쳐서
첫째는 회수되지 않는 항목이었다. 물리 삭제를 수행하고 데이터베이스를 정산하기 전에 죽은 작업자가 행을 진행 중 상태로 영원히 남겼다. 어떤 질의도 그 항목을 살아 있는 작업자가 지금 지우고 있는 항목과 구별할 수 없었다. 파일은 이미 사라졌는데 쿼터와 수명주기는 정산되지 않은 채 남았다.
둘째는 덮어쓰기였다. 완료 갱신이 정리 식별자만으로 행을 찾았다. 어떤 합리적 리스보다 오래 멈춰 있던 작업자가 깨어나, 그 사이 다른 작업자가 청구해 반쯤 진행한 항목 위에 완료를 쓸 수 있었다.
같은 누락에서 나왔지만 방향이 반대다.
## 컬럼 넷과 질의 둘
:::evidence key="a-cleanup-claim-without-fencing" alt="코드베이스에서 V3 마이그레이션의 컬럼 넷과 정리 저장소의 질의, 그리고 claim_fence 참조를 뽑은 출력 21줄. 완료 갱신이 토큰으로 행을 찾고 회수기가 널 만료를 제외한다는 것, claimFence 는 증가만 하고 그 엔티티의 getter 열셋 안에 없다는 것이 그 출력에 그대로 보인다." caption="V3 컬럼 넷 · 토큰 대조 · 널 만료 제외 · claimFence 참조 — 21줄 · exit 0" zoom="true"
:::
컬럼은 `claim_owner` · `claim_token` · `lease_until` · `claim_fence` 다. 앞의 셋은 널을 허용하고 넷째만 `NOT NULL DEFAULT 0` 이다. 헤더가 그 이유를 하나로 적는다 — 이 마이그레이션 이전에 청구된 항목이 계속 동작해야 하기 때문이다.
스키마만 바뀐 것이 아니다. 저장소의 질의 둘이 그 컬럼을 실제로 쓴다.
## 토큰이 덮어쓰기를 막는다
완료 갱신의 `where` 절에 `and c.claimToken = :token` 이 붙었다. 그 메서드의 javadoc 이 이전 상태를 적는다 — 예전에는 정리 식별자만으로 대조했고, 그래서 오래 멈춰 있던 작업자가 다른 작업자의 진행 중 항목 위에 완료를 쓸 수 있었다.
반환 타입이 `int` 다. 교체된 작업자는 예외를 받는 것이 아니라 **갱신 행 수 0** 을 받는다. 행을 못 찾는 것이 실패가 아니라 답이 되는 형태다.
## 널 만료를 회수기가 건너뛴다
회수기 질의는 진행 중이면서 만료가 지난 항목을 만료 순으로 가져온다. 조건에 `and c.leaseUntil is not null` 이 들어 있다.
그 메서드의 javadoc 이 이유를 적는다 — 널 리스는 펜싱이 생기기 전에 청구됐다는 뜻이고, 자동으로 넘겨받는 것은 아무도 상태를 기록하지 않은 작업에 대해 추측하는 일이다. 그 항목이 물리 삭제를 마쳤는지 시작도 안 했는지 알 방법이 없다. 그래서 운영자를 필요로 한다.
부분 인덱스가 그 질의 모양 그대로 만들어져 있다 — `lease_until` 에 걸리고 조건이 `status = 'IN_PROGRESS'` 다.
## 넷째 컬럼은 아직 소비자가 없다
`claim_fence` 는 청구할 때마다 1 씩 오른다. 저장소에서 그 이름이 나오는 곳은 그 증가 한 줄과 엔티티의 필드 선언뿐이다.
엔티티는 getter 열셋을 갖는다. `getClaimToken``getLeaseUntil` 은 있고 `claimFence` 의 getter 는 없다. 즉 값이 올라가기만 하고 어디에서도 읽히거나 비교되지 않는다.
마이그레이션은 이 컬럼에만 COMMENT 를 붙이지 않았다. 다른 둘에는 무엇을 위한 값인지 적혀 있다.
## 확인하지 못한 것
작업자를 죽여 진행 중 항목이 남는 것을 재현하지 않았다. 컨테이너 레인을 돌리지 않았다.
<!-- body:end -->
@@ -1,114 +0,0 @@
---
kind: CASE
slug: a-read-then-delete-race-on-the-upload-lease
title: cleanup이 읽은 lease와 삭제 사이에 writer가 그 lease를 얻을 수 있었다
topic: fileserver-state-and-fencing
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:a-read-then-delete-race-on-the-upload-lease
evidenceCapturedOn: 2026-09-02
assets:
- key: a-read-then-delete-race-on-the-upload-lease
file: ../../../final/evidence/rendered/a-read-then-delete-race-on-the-upload-lease.svg
evidence:
- ../../../final/evidence/raw/a-read-then-delete-race-on-the-upload-lease.txt
source:
- 이 경합의 1차 기록은 `V4__fileserver_upload_terminal_state.sql` 의 헤더다. 분석 문서는 두 곳에서 이 사건을 다룬다. application-core 편 §11.2 가 취소와 정리의 경합을 청구가 만드는 경계로 정리하고, persistence-jpa 편 §83.3 이 이전 리뷰가 올린 소유자·토큰·최종 상태 부재를 V3·V4 와 현재 저장소 코드가 해소했다고 적는다. 같은 편 §79 의 마이그레이션 목록도 이 컬럼을 언급한다.
---
# cleanup이 읽은 lease와 삭제 사이에 writer가 그 lease를 얻을 수 있었다
정리 작업이 쓰기 리스를 읽어 없음을 확인하고 스테이징 바이트를 지웠다. 읽기와 삭제 사이에 쓰기 작업자가 바로 그 리스를 얻을 수 있었고, 정리가 지운 것은 업로드가 이어 쓰고 있던 객체였다.
## 관계
- **조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다**
이 사례에서 끌어낸 규칙이다.
- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다**
같은 형태를 다른 저장소에서 다룬 규칙이다.
- **claim이 소유자·토큰·만료를 기록하지 않아 죽은 worker의 항목이 영영 남았다**
같은 어댑터의 앞선 마이그레이션이 다룬 문제다.
## 문제
정리 작업은 스테이징 바이트를 지우기 전에 그 업로드에 쓰기 리스가 걸려 있는지 확인했다. 리스가 없으면 아무도 쓰고 있지 않다고 판단하고 지웠다.
읽기와 삭제 사이에 쓰기 작업자가 그 리스를 얻을 수 있었다. 업로드가 끝났다는 표시가 데이터베이스에 없었기 때문이다.
쓰기 작업자의 획득 문장도 그것을 막지 못했다. 그 문장은 업로드의 만료와 리스만 확인했고, 취소되었는지 검증에 실패했는지는 확인하지 않았다.
## 결론
수정은 두 쪽이 함께 볼 상태 컬럼을 만드는 것이었다. 취소와 검증 실패 마무리가 정리를 큐에 넣는 같은 트랜잭션 안에서 세션을 최종 상태로 옮기고, 획득과 갱신과 오프셋 커밋은 활성 상태를 요구한다.
정리는 조금 전에 읽은 값으로 판단하는 대신 조건부 갱신으로 행을 청구한다. 0행이 돌아오면 삭제를 다음 주기로 넘긴다.
기본값이 활성으로 잡혀 있어 이 컬럼이 생기기 전 세션도 영향을 받지 않는다.
리스 반납 문장에는 이 조건이 없다. 클래스 javadoc 은 모든 쓰기 문장이 활성을 요구한다고 적는데, 그 문장 하나는 그렇지 않다.
## 검증 환경
데이터베이스 : PostgreSQL
마이그레이션 도구 : Flyway
확인 방식 : 마이그레이션 헤더의 사후 기록과 현재 구현의 조건절·트랜잭션 경계 확인
소스 수정 : x
## 재현 조건
1. 업로드 최종 상태 마이그레이션의 헤더를 읽는다. 경합 순서와 양쪽의 판단 근거가 적혀 있다.
2. 추가된 상태 컬럼과 CHECK 제약, 부분 인덱스를 확인한다.
3. 저장소의 쓰기·전이 문장을 전부 세고, 그중 몇 개가 활성 상태를 조건으로 거는지 확인한다.
4. 클래스 javadoc 의 주장과 각 문장의 실제 조건을 대조한다.
5. 최종화와 정리 큐잉을 감싸는 트랜잭션 람다를 두 경로에서 시작부터 끝까지 읽는다.
6. 정리가 청구 전에 무엇을 하는지, 청구에 실패하면 무엇을 하는지 확인한다.
## 본문
<!-- body:start -->
마이그레이션 헤더가 이 경합을 사후에 기록한다. 정리는 리스의 부재를 봤고, 쓰기의 획득 문장은 업로드의 만료와 리스를 봤다. 취소되었는지 검증에 실패했는지를 묻는 쪽은 없었다.
그래서 이미 삭제 예정인 바이트에 대해 획득 문장이 리스를 내주었다. 그 요청을 거를 근거를 갖고 있지 않았다.
## 상태 하나를 두 쪽이 같이 본다
:::evidence key="a-read-then-delete-race-on-the-upload-lease" alt="코드베이스에서 V4 마이그레이션이 더한 상태 컬럼과 CHECK 제약과 부분 인덱스, 이 저장소의 쓰기·전이 문장 여섯과 그중 활성 상태를 조건으로 거는 네 줄, 모든 쓰기 문장이 활성을 요구한다고 적은 클래스 javadoc 과 그 조건이 없는 리스 반납 문장, 최종화와 정리 청구가 요구하는 상태, 취소 경로와 검증 실패 경로의 트랜잭션 람다 전체, 그리고 정리가 세션을 먼저 읽고 있을 때만 청구하는 조건을 뽑은 출력. javadoc 의 주장과 리스 반납 문장의 조건이 어긋난다는 것이 그 출력에 나란히 보인다." caption="상태 컬럼과 제약 · 문장 여섯과 활성 조건 넷 · javadoc 과 어긋나는 반납 문장 · 두 경로의 트랜잭션 전체 · 정리의 읽기와 청구" zoom="true"
:::
V4 가 세션 테이블에 상태 컬럼을 더한다. 값은 활성과 최종 둘이고 CHECK 로 묶여 있다. 기본값이 활성이라 이 컬럼 이전의 세션은 그대로 동작한다.
같은 마이그레이션이 부분 인덱스도 만든다. 최종 상태인 행만 담고 리스 만료로 정렬한다. 청구 문장의 술어와 같은 조건이지만, 청구 자체는 업로드 아이디 단건 갱신이라 이 인덱스를 타지 않는다. 최종 상태 집합을 만료 순으로 훑을 때를 위한 모양이다.
## 쓰기 문장 넷 중 셋이 활성을 요구한다
리스 획득과 갱신과 오프셋 커밋의 조건절에 활성 상태가 들어갔다. 최종 상태로 옮겨진 세션에는 이 셋이 걸리지 않는다.
리스 반납 문장에는 없다. 업로드 아이디와 리스 토큰만 본다. 최종 상태로 옮겨진 뒤에도 진행 중이던 쓰기 작업자가 자기 리스를 놓을 수 있어야 하고, 반납은 리스 컬럼을 비울 뿐 바이트를 건드리지 않는다.
클래스 javadoc 은 모든 쓰기 문장이 활성을 요구한다고 적는다. 넷 중 셋이다.
최종화 문장 자체도 활성일 때만 성립한다. 이미 최종인 행을 다시 최종으로 옮기는 시도는 0행을 갱신한다.
## 최종화와 정리 큐잉이 한 트랜잭션 안에 있다
취소 경로와 검증 실패 경로가 각각 하나의 트랜잭션 안에서 세 가지를 함께 한다. 기록을 도달 불가로 옮기고, 세션을 최종 상태로 옮기고, 정리 요청을 큐에 넣는다.
셋이 함께 커밋되므로 큐에 항목이 있는데 세션은 아직 활성인 순간이 없다. 취소 경로의 주석이 그 이유를 적는다 — 큐잉만 하면 세션이 활성으로 남아, 정리가 지우려는 바로 그 바이트에 쓰기가 리스를 얻을 수 있었다는 것이다.
첫 번째 쓰기에도 이유가 붙어 있다. 기록이 먼저 도달 불가가 되어야 물리 작업이 예약되고, 둘이 함께 커밋되므로 파일이 도달 불가인데 회수할 것이 큐에 없는 상태가 남지 않는다.
## 정리는 읽은 값 대신 청구로 판단한다
정리 청구 문장은 최종 상태이고 리스가 없거나 만료된 행만 갱신한다. 그 한 문장이 리스가 걸려 있지 않다는 것을 증명하는 동시에 리스 컬럼을 비운다.
청구가 0행이면 지우지 않는다. 항목을 실패로 표시하고 사유를 남기고 결과를 건너뜀으로 돌려준다.
다만 청구는 세션 행이 있을 때만 걸린다. 조건이 존재 확인과 청구의 논리곱이라, 세션이 이미 사라진 항목은 청구를 거치지 않고 바로 지운다 — 지킬 리스를 가진 쓰기 작업자가 있을 수 없는 경우다.
## 확인하지 못한 것
읽기와 삭제 사이에서 리스를 가로채는 경합을 재현해 보지는 않았다. 경합 순서는 마이그레이션 헤더의 사후 기록이고, 수정의 현재 형태는 코드로 확인했다.
<!-- body:end -->
@@ -1,55 +0,0 @@
---
kind: PROJECT_DECISION
slug: no-physical-paths-in-metadata
title: 물리 경로와 원본 파일명을 저장하지 않는다
topic: fileserver-state-and-fencing
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: decision:no-physical-paths-in-metadata
decisionStatus: ADOPTED
decidedOn: 2026-08-30
source:
- src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V1__create_fileserver_metadata.sql
- final/document.md#a08
---
# 물리 경로와 원본 파일명을 저장하지 않는다
## 결정문
파일 메타데이터에 물리 경로도 마운트도 원본 물리 파일명도 저장하지 않는다. 콘텐츠 키는 서버가 만든 불투명 키이고 원본 이름은 표시용 텍스트일 뿐이다.
## 판단 이유
물리 경로를 저장하면 그 값이 언젠가 경로 조작에 쓰인다. 저장소에서 읽은 값이라는 이유로 신뢰되기 쉽고, 그것을 만든 것은 결국 업로드한 쪽이다.
원본 파일명도 같다. 사용자가 정하는 값이고, 그것으로 파일을 찾거나 열면 경로 순회와 확장자 기반 오판의 입구가 된다.
그래서 두 값의 역할을 분리한다. 서버가 만든 불투명 키가 자원을 지목하고, 원본 이름은 화면에 보여 줄 때만 쓴다.
마운트를 저장하지 않는 것도 같은 계열이다. 저장 위치가 메타데이터에 박히면 저장소를 옮길 때 레코드를 고쳐야 하고, 그 값이 코드 경로로 흘러들 수 있다.
## 영향
감수하는 것
원본 이름으로 파일을 찾을 수 없다. 검색이 필요하면 별도 색인이 필요하다.
저장 위치를 메타데이터에서 알 수 없으므로, 키에서 위치를 유도하는 규칙이 어딘가에 있어야 한다.
얻는 것
경로 순회와 확장자 기반 오판의 입구가 메타데이터에 없다.
저장소를 옮겨도 메타데이터를 고치지 않는다.
## 근거
- **파일 상태 기계와 READY가 뜻하는 것**
이 결정이 속한 메타데이터 설계다.
- **sanitize가 아니라 reject가 기본이다**
신뢰할 수 없는 값을 다루는 같은 계열의 규칙이다.
- **이름은 값이 아니라 registry key다**
식별자와 표시용 값을 구별하는 같은 원칙이다.
@@ -1,55 +0,0 @@
---
kind: REFERENCE
slug: a-publicly-readable-state-must-be-complete-by-constraint
title: 공개 읽기 가능한 상태는 완전한 identity를 DB 제약으로 요구한다
topic: fileserver-state-and-fencing
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:a-publicly-readable-state-must-be-complete-by-constraint
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 공개 읽기 가능한 상태는 완전한 identity를 DB 제약으로 요구한다
## 목적
공개 상태에 도달한 레코드가 필수 값을 빠뜨린 채 존재해, 그 값을 전제한 코드가 나중에 실패하는 것을 막는다.
## 규칙
1. 공개 상태의 요구를 데이터베이스 제약으로 표현한다
애플리케이션 검사로 두면 그 검사를 지나지 않는 경로가 언젠가 생긴다.
2. 상태별로 다른 요구를 조건부 제약으로 쓴다
모든 상태에 같은 요구를 걸면 중간 상태를 만들 수 없다.
3. 상태와 버전을 함께 가드한다
상태만 조건에 넣으면 같은 상태에서 출발한 두 전이가 모두 성공한다.
4. 진실의 출처를 하나로 정한다
파일시스템에 바이트가 있다는 것과 공개 가능하다는 것은 다른 사실이다. 어느 쪽이 정본인지 정하고 그것을 문서에 적는다.
## 적용 조건
외부에 노출되는 상태를 갖는 모든 레코드
파일이나 객체처럼 저장소와 메타데이터가 따로 있는 자원
## 예외
내부 처리 단계의 중간 상태는 완전성을 요구하지 않는다. 그 상태가 외부로 새지 않는 것이 보장되어야 한다.
## 예시
파일 메타데이터의 헤더가 관계형 레코드가 공개 가능 여부를 정한다고 명시하고, 모든 상태 전이가 상태와 버전 양쪽으로 가드된다고 적는다.
## 관계
- **파일 상태 기계와 READY가 뜻하는 것**
이 규칙이 나온 개념이다.
- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다**
세 번째 규칙의 일반형이다.
- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다**
같은 원칙의 값 타입 판이다.
@@ -1,60 +0,0 @@
---
kind: REFERENCE
slug: claim-with-a-conditional-update-not-a-read
title: 조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다
topic: fileserver-state-and-fencing
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:claim-with-a-conditional-update-not-a-read
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다
## 목적
읽기와 행동 사이에 상태가 바뀌어, 이미 유효하지 않은 판단으로 되돌릴 수 없는 작업을 수행하는 것을 막는다.
## 규칙
1. 읽고 나서 판단하지 않는다
조건을 갱신문의 where 절에 넣고 갱신 건수로 판단한다.
2. 되돌릴 수 없는 작업 앞에서는 특히 그렇다
물리 삭제나 외부 호출은 되돌릴 수 없다. 그 앞의 판단은 원자적이어야 한다.
3. 양쪽이 같은 사실을 본다
한쪽은 리스의 부재를 보고 다른 쪽은 만료만 보면, 둘 다 자기 기준으로 옳으면서 서로 어긋난다.
4. 상태를 명시적으로 만든다
끝났다는 사실이 값으로 없으면 각 참여자가 그것을 추론하고, 추론의 근거가 서로 다르다.
5. 청구하지 못하면 미룬다
갱신 건수가 0 이면 다른 참여자가 그 행을 들고 있다는 뜻이다. 강제하지 않고 다음 주기로 넘긴다.
## 적용 조건
여러 참여자가 같은 자원을 놓고 경합하는 정리 작업과 배치
물리 삭제나 외부 호출이 뒤따르는 판정
## 예외
읽기와 행동이 같은 트랜잭션 안에서 행 잠금과 함께 일어나면 조건부 갱신 없이도 안전하다. 그 잠금이 실제로 걸리는지 확인해야 한다.
## 예시
정리가 쓰기 리스를 읽어 없음을 확인하고 스테이징 바이트를 지웠다. 읽기와 삭제 사이에 쓰기 작업자가 그 리스를 얻었고, 지워진 것은 업로드가 이어 쓰고 있던 객체였다.
수정 후 정리는 조건부 갱신으로 청구하고, 리스가 실제로 걸려 있으면 아무것도 청구하지 않고 미룬다.
## 관계
- **cleanup이 읽은 lease와 삭제 사이에 writer가 그 lease를 얻을 수 있었다**
이 규칙을 만든 사례다.
- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다**
같은 규칙의 상태 기계 판이다.
- **시간은 DB에서, 그리고 행을 잠근 다음에 읽는다**
같은 계열의 짝 규칙이다.
@@ -1,56 +0,0 @@
---
kind: REFERENCE
slug: prefix-matching-fits-signatures-not-sniffing
title: 접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다
topic: fileserver-state-and-fencing
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:prefix-matching-fits-signatures-not-sniffing
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다
## 목적
파일 형식 시그니처를 찾는 방법으로 브라우저 스니핑 대상을 찾아, 앞에 바이트를 붙이는 것만으로 우회되는 것을 막는다.
## 규칙
1. 두 문제를 구별한다
형식 시그니처는 정의상 시작 바이트다. 브라우저 스니핑은 관용적 해석이므로 시작이 아니어도 된다.
2. 스니핑 대상은 포함으로 찾는다
앞의 일정 구간 안에 마커가 있으면 탐지한다. 시작이어야 한다는 조건을 걸지 않는다.
3. 앞에 붙는 것들을 목록으로 갖는다
바이트 순서 표시와 널 바이트와 공백과 주석이 흔하다.
4. 탐지 대상은 소비자의 관용도에 맞춘다
무엇을 실행할지 정하는 것은 브라우저다. 우리 파서가 아니다.
5. 같은 함수를 두 목적에 쓰지 않는다
한쪽에 맞추면 다른 쪽이 틀린다.
## 적용 조건
업로드 콘텐츠 검증
인라인으로 제공될 수 있는 모든 콘텐츠의 분류
## 예외
형식 시그니처를 확인해 파일 타입을 판정하는 목적이면 시작 매칭이 옳다. 그 경우 그 판정이 보안 결정으로 쓰이지 않아야 한다.
## 예시
실행 가능 콘텐츠 정책이 앞의 1024 바이트에서 마커를 찾되 시작 매칭을 쓴다. 바이트 순서 표시나 널 바이트나 주석을 앞에 붙이면 탐지되지 않고, 브라우저는 그런 파일도 실행한다.
## 관계
- **scriptable 콘텐츠 탐지가 BOM과 NUL과 주석으로 우회된다**
이 규칙을 만든 사례다.
- **sanitize가 아니라 reject가 기본이다**
탐지된 콘텐츠를 어떻게 다룰지 정한 규칙이다.