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:
co-authored by
Claude Opus 5
parent
a0ca2bb72a
commit
1f04117bbf
-111
@@ -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 -->
|
||||
-114
@@ -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 -->
|
||||
-55
@@ -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다**
|
||||
식별자와 표시용 값을 구별하는 같은 원칙이다.
|
||||
|
||||
-55
@@ -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를 답으로 쓴다**
|
||||
세 번째 규칙의 일반형이다.
|
||||
- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다**
|
||||
같은 원칙의 값 타입 판이다.
|
||||
|
||||
-60
@@ -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에서, 그리고 행을 잠근 다음에 읽는다**
|
||||
같은 계열의 짝 규칙이다.
|
||||
|
||||
-56
@@ -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가 기본이다**
|
||||
탐지된 콘텐츠를 어떻게 다룰지 정한 규칙이다.
|
||||
|
||||
Reference in New Issue
Block a user