docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
+117
@@ -0,0 +1,117 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: a-replay-store-with-no-eviction-path
|
||||
title: 재생 저장소에 제거 경로가 없고, 형제 맵의 상한은 세션 경계 하나다
|
||||
topic: retention-and-unbounded-growth
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:a-replay-store-with-no-eviction-path
|
||||
evidenceCapturedOn: 2026-09-02
|
||||
assets:
|
||||
- key: a-replay-store-with-no-eviction-path
|
||||
file: ../../../final/evidence/rendered/a-replay-store-with-no-eviction-path.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/a-replay-store-with-no-eviction-path.txt
|
||||
source:
|
||||
- 분석 문서는 grpc-policy 편 §17.3 과 grpc-advanced-streaming 편 §17.1 이다. 앞의 절이 재생 저장소의 제거 경로 부재를 P2 로 판정하고, 두 모듈의 대조가 "저쪽은 풀었고 이쪽은 안 풀었다"가 아니라는 정정도 그 절에 있다. 뒤의 절이 형제 맵의 증가를 P3 으로 기록하고, 필요한 창이 좁다는 근거를 함께 적는다.
|
||||
- 같은 절이 이 저장소가 커밋될 때마다 항목이 쌓인다고 적는데, 이 클래스를 잡는 프로덕션 코드는 아직 없다. 위 터미널 출력의 참조 수가 그것이다.
|
||||
---
|
||||
|
||||
# 재생 저장소에 제거 경로가 없고, 형제 맵의 상한은 세션 경계 하나다
|
||||
|
||||
결과 재생 저장소가 항목을 넣기만 하고 지우지 않는다. 형제 모듈의 중복 제거기가 같은 형태이고, 그 클래스는 같은 종류의 무제한 증가를 비판하는 javadoc 을 갖고 있다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **bounded 와 unbounded 오버로드를 나란히 둔 포트**
|
||||
같은 Topic 의 보존 경계 문제다.
|
||||
- **같은 안전 규칙은 하나의 공식과 하나의 강제 시점을 갖는다**
|
||||
보존 경계를 어디서 정하는지에 대한 규칙이다.
|
||||
- **runtime_memberships를 먼저 읽고 심각도를 정한다**
|
||||
이 사례가 오늘의 사고가 아닌 이유를 판정하는 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
정확히 한 번을 보장하려면 과거를 기억해야 한다. 같은 요청이 다시 오면 저장된 결과를 돌려주고, 같은 메시지가 다시 오면 건너뛴다.
|
||||
|
||||
그 기억이 자료구조다. 요청 식별자나 메시지 식별자를 키로, 결과나 체크포인트를 값으로 갖는다.
|
||||
|
||||
## 결론
|
||||
|
||||
결과 재생 저장소에는 넣는 경로만 있다. 항목 하나의 크기는 제한하고 개수는 제한하지 않는다. 제거도 비우기도 축출도 만료도 없다.
|
||||
|
||||
형제 모듈의 중복 제거기는 같은 형태에 상한 하나를 갖는다. 세션을 끝낼 때 그 세션 키를 지운다. 그래서 이쪽의 증가는 세션 수명에 묶이고, 저장소 쪽은 프로세스 수명에 묶인다.
|
||||
|
||||
두 모듈의 대조는 한쪽이 풀고 한쪽이 안 푼 것이 아니다. 같은 형태의 무제한 증가를 둘 다 갖고 있고, 한쪽만 부분적 상한을 갖는다. 무거운 쪽은 저장소다.
|
||||
|
||||
이 종류의 자료구조에서 보존 경계는 기능이 아니라 전제다. 무엇을 언제까지 기억하는지가 정해지지 않으면 그 보장은 메모리가 버티는 동안만 성립한다.
|
||||
|
||||
두 모듈 모두 런타임 소속이 비어 있고, 저장소 쪽은 이 클래스를 잡는 프로덕션 코드도 없다. 지금 도는 배포에서 일어나는 문제는 아니다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
Gradle : 9.0.0
|
||||
확인 방식 : 두 자료구조의 삽입 경로와 제거 경로 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 재생 저장소의 맵 선언과 항목을 넣는 메서드를 읽는다.
|
||||
2. 같은 파일에서 제거·비우기·축출·만료를 검색하고, 이 클래스를 잡는 프로덕션 코드를 센다.
|
||||
3. 개수를 노출하는 메서드가 있는지, 그 값을 읽는 프로덕션 코드가 있는지 센다.
|
||||
4. 중복 제거기의 클래스 javadoc 과 두 맵의 증가·감소 지점을 확인한다.
|
||||
5. 중복 제거 키의 형식을 확인해 세션 종료가 실제로 그 세션 항목만 지우는지 본다.
|
||||
6. 두 모듈의 런타임 소속을 모듈 레지스트리에서 읽는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
## 크기는 제한하고 개수는 제한하지 않는다
|
||||
|
||||
:::evidence key="a-replay-store-with-no-eviction-path" alt="코드베이스에서 결과 재생 저장소의 맵 선언과 항목 크기 제한, 제거·비우기·축출·만료 매치 수, 개수를 노출하는 메서드와 그 값을 읽는 코드 수, 이 클래스를 잡는 코드 수, javadoc 이 저장소를 부르는 이름을 뽑은 출력. 이어서 형제 모듈 중복 제거기의 클래스 javadoc 과 두 맵의 선언, 항목이 쌓이는 지점, 재생 판정이 보는 값, 이 파일의 제거 매치 수와 그 둘이 있는 자리, 중복 제거 키의 형식, 그리고 두 모듈의 런타임 소속이 나온다. 저장소 쪽 제거 매치가 0 이고 형제 맵 쪽은 세션 종료 하나뿐이라는 것이 그 출력에 보인다." caption="저장소: 크기 제한만 · 제거 0 · 잡는 코드 0 / 형제 맵: 메시지마다 한 항목 · 제거는 세션 종료 하나 · 키는 세션 접두 — 54줄" zoom="true"
|
||||
:::
|
||||
|
||||
결과 재생 저장소는 결과 참조를 키로, 직렬화된 응답을 값으로 갖는 맵이다.
|
||||
|
||||
넣는 메서드가 크기를 검사한다. 인라인 한도를 넘는 응답은 예외로 거절하고, 객체 참조 뒤에 두라고 메시지에 적는다.
|
||||
|
||||
개수를 검사하는 코드는 없다. 제거도 비우기도 축출도 만료도 파일 전체에서 0 이다.
|
||||
|
||||
이 클래스를 잡는 프로덕션 코드도 아직 0 이다. 배선되면 멱등 키가 필요한 커밋 하나당 항목 하나가 프로세스 수명 동안 남는 형태다.
|
||||
|
||||
## 개수를 볼 수는 있는데 보는 곳이 없다
|
||||
|
||||
개수를 돌려주는 메서드가 하나 있다. 그것을 부르는 프로덕션 코드가 0 이고, 부르는 것은 테스트 하나다.
|
||||
|
||||
javadoc 은 이 저장소를 배포가 고를 수 있는 선택지 중 하나로 소개하면서 작은 인라인 저장소라고 부른다. 작게 유지하는 장치는 그 안에 없다.
|
||||
|
||||
## 형제 모듈이 같은 문제를 문장으로 적어 두었다
|
||||
|
||||
중복 제거기의 클래스 javadoc 이 설계 선택을 설명한다. 본 키를 모으는 집합을 거부한 이유가 셋인데, 첫 번째가 집합은 세션 수명 동안 무제한으로 증가한다는 것이다.
|
||||
|
||||
체크포인트 맵은 그 비판을 지킨다. 세션당 항목 하나이고 순번만 앞으로 간다.
|
||||
|
||||
형제 맵은 지키지 않는다. 적용된 메시지마다 결과 참조를 하나씩 넣는다.
|
||||
|
||||
같은 javadoc 에는 그 맵에 대해 재생 결과가 체크포인트 뒤의 좁은 창 동안만 유지된다고 한 줄 적혀 있다. 창을 닫는 코드가 없다 — 체크포인트가 그 순번을 지나가도 항목은 남는다.
|
||||
|
||||
필요한 창이 실제로 좁다는 것도 코드에 있다. 재생 판정은 체크포인트의 순번 비교가 내리고, 재생 응답에 쓰이는 것은 그 순번의 결과 하나다. 체크포인트보다 한참 뒤처진 순번의 결과가 필요한 구간은 재개 직후뿐이다.
|
||||
|
||||
## 줄어드는 지점은 세션 종료 하나다
|
||||
|
||||
이 파일에서 무언가를 지우는 지점은 둘이고 둘 다 같은 메서드 안에 있다. 세션을 끝내면서 체크포인트를 지우고, 재생 결과 맵에서 그 세션 접두사를 가진 키를 지운다.
|
||||
|
||||
중복 제거 키는 세션 값과 세대와 순번을 막대로 이어 만든다. 접두사 제거가 그 세션의 모든 세대를 함께 지운다.
|
||||
|
||||
그래서 이쪽의 증가는 세션 수명에 묶인다. 저장소 쪽은 프로세스 수명에 묶인다.
|
||||
|
||||
두 모듈 모두 모듈 레지스트리의 런타임 소속이 비어 있다. 배선 경로가 없으니 오늘 이것으로 무너지는 배포는 없다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
오래 돌려 증가를 확인해 보지는 않았다. 배선 경로가 없어 실행 대상이 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: the-cleanup-that-causes-the-outage-it-prevents
|
||||
title: cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다
|
||||
topic: retention-and-unbounded-growth
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:the-cleanup-that-causes-the-outage-it-prevents
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: the-cleanup-that-causes-the-outage-it-prevents
|
||||
file: ../../../final/evidence/rendered/the-cleanup-that-causes-the-outage-it-prevents.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/the-cleanup-that-causes-the-outage-it-prevents.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-inbox-jdbc-postgresql.md §17 P1 · analysis/messaging/messaging-outbox-jdbc-postgresql.md §17 P1 이다.
|
||||
---
|
||||
|
||||
# cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다
|
||||
|
||||
inbox 와 outbox 의 정리 잡이 무제한 삭제 오버로드를 부른다. 배치 제한 구현은 두 리프 모두에 있고 호출 지점이 0이다. 잡의 javadoc 이 실행되는 코드의 동작을 그대로 서술한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **bounded 와 unbounded 오버로드를 나란히 둔 포트**
|
||||
이 사례가 속한 구조다.
|
||||
- **두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다**
|
||||
이 사례가 만든 규칙이다.
|
||||
- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다**
|
||||
회귀 테스트가 성립하지 않는 이유다.
|
||||
|
||||
## 문제
|
||||
|
||||
inbox 와 outbox 는 처리된 행을 보존 기간 동안 남긴다. 그 기간이 지나면 정리 잡이 지운다.
|
||||
|
||||
한 번에 얼마나 지우는지가 이 잡의 안전을 정한다. 몇 주 동안 쌓인 테이블에서 조건에 맞는 행 전부를 한 문장으로 지우면, 그 문장이 끝날 때까지 락을 잡고 백로그 크기에 비례해 WAL 을 쓴다.
|
||||
|
||||
## 결론
|
||||
|
||||
두 정리 잡이 무제한 오버로드를 부른다.
|
||||
|
||||
배치 제한 구현은 두 리프 모두에 있다. 건수 제한과 잠긴 행 건너뛰기를 쓰는 SQL 이고, 저장소 전체에서 그 시그니처가 등장하는 곳은 선언 둘, 구현 둘, 테스트 대역 다섯이며 호출 지점이 0이다.
|
||||
|
||||
inbox 정리 잡의 javadoc 이 실행되는 코드의 동작을 그대로 서술한다.
|
||||
|
||||
> A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent.
|
||||
|
||||
정리가 막으려던 장애를 정리가 일으킨다는 문장이고, 그것이 지금 호출되는 형태다.
|
||||
|
||||
두 리프 다 출하 애플리케이션 소속이고 두 잡 다 스타터 빈이다. 다만 스케줄러가 등록되지 않으며 그것은 의도된 설계다. 그래서 상시 결함이 아니라 잠재 결함이고, 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 첫 스윕에서 발현한다.
|
||||
|
||||
회귀 테스트를 쓰려면 대역도 함께 고쳐야 한다. 현재 대역의 배치 제한 구현은 무제한 결과와 제한값 중 작은 쪽을 돌려주는 형태다. 전부 지우고 숫자만 깎으므로 두 형태의 차이를 재현하지 못한다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
Gradle : 9.0.0
|
||||
확인 방식 : 두 오버로드의 선언과 구현 위치 확인, 저장소 전역 호출 지점 검색
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/294-bounded-purge-never-called.txt 에 있다.
|
||||
|
||||
1. 두 포트에서 정리 연산의 두 오버로드 선언을 확인한다.
|
||||
2. 각 구현의 SQL 에서 건수 제한과 잠금 건너뛰기 유무를 확인한다.
|
||||
3. 배치 제한 시그니처를 저장소 전역에서 검색해 호출 지점을 센다.
|
||||
4. 두 정리 잡이 어느 오버로드를 부르는지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
두 cleanup 잡이 무제한 오버로드를 부른다. bounded 구현은 `LIMIT` + `FOR UPDATE SKIP LOCKED` 로 두 리프 모두에 존재하고 호출 지점이 0 이다.
|
||||
|
||||
## InboxCleanupJob 참조 위치
|
||||
|
||||
:::evidence key="the-cleanup-that-causes-the-outage-it-prevents" alt="코드베이스에서 InboxCleanupJob 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxCleanupJob 코드베이스 검색 — 3줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## javadoc 이 실행되는 코드의 동작을 그대로 서술한다
|
||||
|
||||
"A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent." bounded 쪽 javadoc 은 한 발 더 나가, 배치 크기로 제한된다는 잡의 자기 서술을 참으로 만드는 것이 바로 이 파라미터라고 적는다. 그 파라미터를 아무도 넘기지 않는다.
|
||||
|
||||
## 상시 결함이 아니라 잠재 결함이다
|
||||
|
||||
두 리프 다 `app-bootstrap` 소속이고 두 잡 다 스타터 빈이지만 스케줄러가 등록되지 않는다 — 그것이 의도된 설계다. 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 첫 스윕에서 발현한다.
|
||||
|
||||
## 회귀 테스트가 성립하려면 대역도 고쳐야 한다
|
||||
|
||||
현재 대역의 bounded 구현은 전부 지우고 숫자만 깎는 형태라 차이를 재현하지 못한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
백로그가 쌓인 실제 테이블에서 두 형태의 락 보유 시간을 측정하지 않았다. 호출 지점 부재와 두 SQL 의 형태로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: bounded-and-unbounded-side-by-side
|
||||
title: bounded 와 unbounded 오버로드를 나란히 둔 포트
|
||||
topic: retention-and-unbounded-growth
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:bounded-and-unbounded-side-by-side
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: bounded-and-unbounded-side-by-side
|
||||
file: ../../../final/evidence/rendered/bounded-and-unbounded-side-by-side.svg
|
||||
- key: bounded-and-unbounded-side-by-side-diagram
|
||||
file: ../../../final/assets/diagrams/bounded-and-unbounded-side-by-side.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/bounded-and-unbounded-side-by-side.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-reliability-api.md §17 · analysis/messaging/messaging-inbox-jdbc-postgresql.md §17 이다.
|
||||
---
|
||||
|
||||
# bounded 와 unbounded 오버로드를 나란히 둔 포트
|
||||
|
||||
두 포트가 각각 정리 연산의 무제한 형태와 배치 제한 형태를 오버로드로 나란히 선언한다. 두 형태를 가르는 표시가 없어서 호출자가 인자가 적은 쪽을 골랐다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다**
|
||||
이 구조에서 나온 규칙이다.
|
||||
- **cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다**
|
||||
이 구조가 실제로 발현한 사례다.
|
||||
- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다**
|
||||
같은 계열의 기존 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
두 포트가 각각 `purge*Before(Instant)` 와 `purge*Before(Instant, int)` 를 나란히 선언한다. 뒤쪽이 안전한 형태이고 그 이유가 javadoc 에 있다 — "The cleanup jobs describe themselves as bounded by batch size; this is the parameter that makes that true." 두 형태를 가르는 표시는 없다.
|
||||
|
||||
## 두 오버로드를 가르는 표시
|
||||
|
||||
:::evidence key="bounded-and-unbounded-side-by-side-diagram" alt="무제한 오버로드와 가르는 표시 없음이 왼쪽에, 배치 제한 오버로드와 javadoc 근거가 오른쪽에 같은 축으로 놓인다" caption="두 오버로드를 가르는 표시" zoom="false"
|
||||
:::
|
||||
|
||||
## 호출자가 인자가 적은 쪽을 고른다
|
||||
|
||||
`@Deprecated` 도, 이름 차이도, 호출을 막는 가시성 차이도 없다.
|
||||
|
||||
## 두 오버로드의 javadoc
|
||||
|
||||
:::evidence key="bounded-and-unbounded-side-by-side" alt="분석 문서 analysis/messaging/messaging-reliability-api.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-reliability-api.md 발췌 — 18줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 결함이 호출자에게 있지 않다
|
||||
|
||||
같은 리프가 다른 곳에서 같은 형태를 이미 기록했다 — 한 인터페이스가 같은 전이의 두 세대를 갖고 안전하지 않은 쪽에 표시가 없다. 포트의 형태가 오용을 가능하게 했고, 두 리프에서 같은 방향으로 발생했다.
|
||||
|
||||
:::note
|
||||
|
||||
없음 — 두 포트의 선언과 저장소 전역 호출 지점을 전수 확인했다
|
||||
|
||||
:::
|
||||
|
||||
## 두 형태
|
||||
|
||||
정리 연산은 기준 시각 이전의 행을 지운다. 무제한 형태는 시각만 받고, 배치 제한 형태는 시각과 최대 건수를 받는다.
|
||||
|
||||
두 형태의 차이는 SQL 에서 드러난다. 제한 형태는 건수 제한과 잠긴 행 건너뛰기를 함께 쓴다. 한 번에 지우는 양이 정해져 있으므로 락 보유 시간과 WAL 기록량이 백로그 크기와 무관해진다.
|
||||
|
||||
## 안전한 쪽이 무엇을 참으로 만드는가
|
||||
|
||||
배치 제한 오버로드의 javadoc 이 자기 존재 이유를 적는다.
|
||||
|
||||
> The cleanup jobs describe themselves as bounded by batch size; this is the parameter that makes that true.
|
||||
|
||||
정리 잡이 자신을 배치 크기로 제한된다고 서술하는데, 그것을 참으로 만드는 것이 바로 이 파라미터라는 뜻이다.
|
||||
|
||||
## 표시가 없다
|
||||
|
||||
두 형태를 가르는 것이 인자 개수뿐이다. 폐기 표시도, 이름 차이도, 호출을 막는 가시성 차이도 없다.
|
||||
|
||||
그래서 호출자는 짧은 쪽을 고른다. 그것은 호출자의 부주의가 아니라 포트가 만든 기본값이다. 인자를 하나 더 넘기려면 그 값을 어디서 가져올지 정해야 하고, 그 결정을 미루는 것이 자연스러운 선택이 된다.
|
||||
|
||||
## 같은 리프가 같은 형태를 이미 기록했다
|
||||
|
||||
이 포트에는 같은 전이의 두 세대 메서드도 나란히 있다. 안전하지 않은 쪽에 폐기 표시가 없고, production 은 신세대만 쓰지만 그것을 강제하는 것은 없다.
|
||||
|
||||
두 사례가 한 인터페이스 안에 있다는 점이 이 개념의 요지다. 포트의 형태가 오용을 가능하게 했고, 같은 방향으로 두 번 발생했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: a-fake-that-cannot-show-the-property-is-not-a-witness
|
||||
title: 그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다
|
||||
topic: retention-and-unbounded-growth
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:a-fake-that-cannot-show-the-property-is-not-a-witness
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
---
|
||||
|
||||
# 그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다
|
||||
|
||||
## 목적
|
||||
|
||||
테스트 대역이 실제 구현의 불변식을 재현하지 못하면 그 위에서 통과한 단언은 그 불변식에 대해 아무 말도 하지 않는다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 단언하는 속성이 협력자의 상태 변화에 달려 있는지 본다
|
||||
달려 있지 않으면 이 규칙은 해당하지 않는다.
|
||||
|
||||
2. 대역이 결함 있는 구현과 올바른 구현을 구분할 수 있는지 묻는다
|
||||
구분하지 못하면 그 테스트는 회귀를 잡지 못한다.
|
||||
|
||||
3. 대역이 흉내 내는 불변식을 문장으로 쓴다
|
||||
쓸 수 없으면 대역이 무엇을 재현하는지 저자도 모르는 상태다.
|
||||
|
||||
4. 이름이 속성을 주장하면 대역을 먼저 읽는다
|
||||
이름이 강할수록 그 공백이 눈에 띄지 않는다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
저장소, 브로커, 파일 시스템처럼 상태를 갖는 협력자의 인메모리 대역 전부.
|
||||
|
||||
## 예외
|
||||
|
||||
협력자의 존재만 필요한 테스트에는 해당하지 않는다. 호출이 일어났는지만 확인하는 경우가 그렇다.
|
||||
|
||||
## 예시
|
||||
|
||||
배치 제한 정리 대역이 무제한 결과와 제한값 중 작은 쪽을 돌려준다. 전부 지우고 숫자만 깎으므로 두 형태의 차이를 보일 수 없다.
|
||||
|
||||
리드라이브 대역의 정착 메서드가 정착 목록에 추가만 하고 준비 목록을 줄이지 않는다. 실제 불변식은 정착된 것이 다음 조회에서 사라지는 것이고, 재개 지점 계산이 정확히 그 성질에 달려 있다.
|
||||
|
||||
청구 대역의 저장 메서드가 삽입을 흉내 내서, 실제 구현이 갱신으로 동작한다는 차이를 가린다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다**
|
||||
이 규칙이 회귀 테스트를 막는 사례다.
|
||||
- **재개된 리드라이브가 옮기지 못한 메시지를 건너뛰고 성공으로 닫힌다**
|
||||
대역이 불변식을 재현하지 못해 결함이 오래 남은 사례다.
|
||||
- **배정 식별자 때문에 insert-first 청구가 UPSERT 가 되어 커밋된 결과를 덮었다**
|
||||
같은 형태의 세 번째 사례다.
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: a-port-that-offers-both-forms-has-chosen-the-unsafe-one
|
||||
title: 두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다
|
||||
topic: retention-and-unbounded-growth
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:a-port-that-offers-both-forms-has-chosen-the-unsafe-one
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
---
|
||||
|
||||
# 두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다
|
||||
|
||||
## 목적
|
||||
|
||||
인터페이스가 만든 기본값을 호출자의 선택으로 오인하지 않는다. 안전하지 않은 형태가 컴파일되면 그것은 언젠가 호출된다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 안전하지 않은 쪽을 부르는 것이 컴파일되는지 본다
|
||||
컴파일되면 그 형태는 기본값이다.
|
||||
|
||||
2. 두 형태를 가르는 표시가 있는지 본다
|
||||
폐기 표시, 이름 차이, 가시성 차이 중 아무것도 없으면 호출자는 짧은 쪽을 고른다.
|
||||
|
||||
3. 두 형태가 진짜로 다른 용도인지 묻는다
|
||||
다르면 이름이 그 차이를 말해야 한다. 오버로드로 두는 것은 차이를 이름에서 지우는 선택이다.
|
||||
|
||||
4. 같은 용도라면 하나를 지운다
|
||||
지울 수 없으면 폐기 표시와 함께 부를 조건을 javadoc 에 적는다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
정리, 삭제, 조회처럼 결과 크기가 데이터 양에 비례하는 모든 연산. 그리고 같은 전이의 두 세대가 공존하는 인터페이스.
|
||||
|
||||
## 예외
|
||||
|
||||
두 형태가 서로 다른 호출자 집단을 위한 것이면 공존이 맞다. 그때 판정 기준은 이름이 그 구분을 담고 있는지다.
|
||||
|
||||
## 예시
|
||||
|
||||
inbox 와 outbox 포트가 정리 연산의 무제한 형태와 배치 제한 형태를 오버로드로 나란히 선언한다. 두 정리 잡이 무제한 쪽을 부르고, 배치 제한 쪽은 호출 지점이 0이다.
|
||||
|
||||
같은 포트에 같은 전이의 두 세대 메서드가 있다. 안전하지 않은 쪽에 폐기 표시가 없다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **bounded 와 unbounded 오버로드를 나란히 둔 포트**
|
||||
이 규칙이 나온 구조다.
|
||||
- **cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다**
|
||||
이 규칙을 어긴 결과다.
|
||||
- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다**
|
||||
같은 계열의 규칙이고, 이쪽은 어휘가 아니라 시그니처에 대한 것이다.
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: one-formula-and-one-enforcement-point-per-safety-rule
|
||||
title: 같은 안전 규칙은 하나의 공식과 하나의 강제 시점을 갖는다
|
||||
topic: retention-and-unbounded-growth
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:one-formula-and-one-enforcement-point-per-safety-rule
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
---
|
||||
|
||||
# 같은 안전 규칙은 하나의 공식과 하나의 강제 시점을 갖는다
|
||||
|
||||
## 목적
|
||||
|
||||
같은 종류의 안전 여유가 여러 곳에서 독립적으로 정해지면 공식과 강제 시점이 갈린다. 값의 주인을 한 곳으로 정한다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 이 값을 바꾸려면 몇 군데를 고쳐야 하는지 센다
|
||||
하나가 아니면 나머지는 조용히 옛 값으로 남는다.
|
||||
|
||||
2. 공식이 같은지 본다
|
||||
같은 관계를 곱셈과 덧셈으로 다르게 표현하고 있으면 둘 중 하나는 근거가 없다.
|
||||
|
||||
3. 강제 시점이 같은지 본다
|
||||
한쪽은 항상 검증하고 다른 쪽은 특정 객체를 만들 때만 검증하면, 그 객체를 만들지 않는 배포는 검증을 받지 않는다.
|
||||
|
||||
4. 값이 아니라 관계가 소유자를 가져야 하는 경우를 구분한다
|
||||
층마다 다른 값이 정당하면 값이 아니라 그 관계를 한 곳에 두고 검증한다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
보존 기간, 마감, 재시도 상한, 배치 크기처럼 안전을 위해 고른 모든 수치.
|
||||
|
||||
## 예외
|
||||
|
||||
클라이언트 마감이 서버 마감보다 짧아야 하는 것처럼 층마다 다른 값이 필요한 경우. 그때는 값이 아니라 관계가 한 곳에 있어야 하고, 그 관계를 검증하는 코드가 있어야 한다.
|
||||
|
||||
## 예시
|
||||
|
||||
inbox 보존 여유가 한 곳에서는 곱셈으로, 다른 곳에서는 덧셈으로 표현된다. 한쪽은 정리 잡을 만들 때만 검증하고 다른 쪽은 항상 검증한다.
|
||||
|
||||
드레인 마감 30초가 세 곳에서 각자 정해진다. 공개 상수 하나, 다른 클래스의 비공개 복사본 하나, 생성자 인자 하나. 테스트가 붙드는 것은 공개 상수뿐이고, 살아 있는 값은 비공개 복사본이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **재생 저장소에 제거 경로가 없고, 형제 맵의 상한은 세션 경계 하나다**
|
||||
보존 경계가 아예 없는 쪽의 사례다.
|
||||
- **순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다**
|
||||
드레인 마감 세 곳이 그 사건의 일부다.
|
||||
- **문서의 수치는 세지 말고 파생하거나 게이트로 붙든다**
|
||||
같은 규칙의 문서 판이다.
|
||||
|
||||
Reference in New Issue
Block a user