docs(TechLog): SSOT 에 도메인과 비즈니스 규칙을 넣는다
이 문서는 결함 카탈로그로만 있었고 이 시스템이 무엇을 하는지가 없었다. 두 저장소의 도메인·유스케이스에서 확인해 §1.4 를 세웠다. 종류 다섯이 각자 자기 애그리거트와 테이블을 갖는다 (ADR-003) — 개념은 문서 테이블에 얹히고 concept_detail 에 기준 버전만 따로 둔다. §3 의 열세 건과 §14 의 외래키 없는 축 표가 여기서 나온다 화면 상태와 도메인 상태가 다르다 — 질문의 OPEN·INVESTIGATING·PAUSED 가 화면에서 하나로 접히고 결정의 ACCEPTED 가 ADOPTED 로 보인다 NextAction 판정 순서 — 서버가 조회 시점에 계산하고 저장하지 않는다. 프론트가 여러 endpoint 를 조합해 workflow 를 재추론하지 않게 하려는 것이다 검증·미리보기가 버려지지 않는 산출물인 이유와 그것이 유효한 조건 셋 게시가 단계마다 다른 코드로 거절하는 이유 — 고칠 것과 다시 검증할 것이 다르다 저장할 때와 공개할 때의 요구가 다르다 · 문서가 아닌 것은 다른 경로로 공개된다 · 주제는 참조가 있으면 안 지우고 축은 연결만 끊는다 · 없는 것을 가리키는 설정을 막는다 SSOT 62,643 → 71,870 자. 인용은 전부 저장소의 javadoc 과 코드에서 옮겼다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fd221353a3
commit
4769e52e48
@@ -114,6 +114,181 @@ import 합니다. 배포 단위는 `hyeonworks.com`(prod) 하나이고 서브도
|
||||
|
||||
---
|
||||
|
||||
## 1.4 이 저장소가 다루는 것 — 기록 하나가 공개되기까지
|
||||
|
||||
결함을 읽기 전에 이 시스템이 무엇을 하는지가 필요합니다. 여기서 다루는 것은 기술 기록이고,
|
||||
기록 하나는 작성자가 쓰기 시작해서 공개 사이트에 뜨기까지 정해진 단계를 지납니다.
|
||||
|
||||
### 종류 다섯은 각자 자기 테이블을 갖는다
|
||||
|
||||
`RecordKind` 는 다섯입니다 — `CASE` · `REFERENCE` · `QUESTION` · `PROJECT_DECISION` ·
|
||||
`CONCEPT`. 이것은 편집 화면이 고르는 유형이고 도메인 애그리거트가 아닙니다.
|
||||
|
||||
> Studio 편집 대상 유형. 계약(`studio-v1.yaml` `RecordKind`)의 discriminator이며 Domain
|
||||
> Aggregate가 아니다 — ADR-003대로 각 유형은 각자 자기 Aggregate와 테이블을 그대로 소유한다.
|
||||
>
|
||||
> `CONCEPT`은 `document`의 세 번째 유형이다 — Case/Reference와 테이블을 공유하고
|
||||
> `concept_detail`에 기준 버전만 따로 둔다.
|
||||
|
||||
그래서 「종류를 하나 더한다」가 테이블 하나를 더하는 일이 아닙니다. 개념은 문서 테이블에 얹히고
|
||||
질문과 결정은 각자 다른 테이블에 있습니다. §3 의 열세 건과 §14 의 축 표에 외래키가 없는 것이
|
||||
전부 이 구조에서 나옵니다.
|
||||
|
||||
### 화면 이름과 도메인 상태는 다른 값이다
|
||||
|
||||
계약이 화면에 주는 상태와 도메인이 들고 있는 상태가 같지 않습니다.
|
||||
|
||||
| 계약이 주는 값 | 도메인이 들고 있는 값 |
|
||||
|---|---|
|
||||
| `QuestionStatus` : OPEN · RESOLVED | OPEN · INVESTIGATING · PAUSED · RESOLVED |
|
||||
| `DecisionStatus` : PROPOSED · ADOPTED | PROPOSED · ACCEPTED |
|
||||
|
||||
질문 상태 셋이 화면에서 하나로 접히고, 그 역방향 변환은 도메인 상태를 덮어쓰지 않습니다. 결정의
|
||||
`ACCEPTED` 는 화면에서 `ADOPTED` 로 보입니다. §13.2 에서 종류 이름을 두 번 바꾸면서 계약의
|
||||
`kind` 를 건드리지 않은 것과 같은 갈래입니다.
|
||||
|
||||
### 작성에서 공개까지 — 서버가 한 값으로 답한다
|
||||
|
||||
작성자가 다음에 무엇을 해야 하는지를 서버가 조회 시점에 계산해 한 값으로 줍니다.
|
||||
|
||||
```java
|
||||
if (!isWorthValidating(document)) {
|
||||
return NextAction.CONTINUE_EDITING;
|
||||
}
|
||||
if (!isValidationCurrent(validation, document, currentDependencyRevision, now)) {
|
||||
return NextAction.VALIDATE;
|
||||
}
|
||||
if (validation.status() == ValidationStatus.INVALID) {
|
||||
return NextAction.FIX_VALIDATION;
|
||||
}
|
||||
if (!isPreviewCurrent(preview, document, currentDependencyRevision, now)) {
|
||||
return NextAction.CREATE_PREVIEW;
|
||||
}
|
||||
if (publication == null
|
||||
|| publication.status() != PublicationAggregateStatus.PUBLISHED
|
||||
|| publication.publishedVersion() != document.version()) {
|
||||
return NextAction.PUBLISH;
|
||||
}
|
||||
return NextAction.NONE;
|
||||
```
|
||||
|
||||
앞선 조건이 참이면 뒤는 보지 않습니다. 이 값을 저장하지 않고 조회할 때마다 계산하는 이유가
|
||||
클래스 javadoc 에 있습니다:
|
||||
|
||||
> 서버가 조회 시점에 계산하는 Studio projection이며 어떤 domain 컬럼에도 저장하지 않는다.
|
||||
>
|
||||
> 프론트가 여러 endpoint를 조합해 workflow 상태를 재추론하지 않도록 서버가 단일 값으로 답한다.
|
||||
|
||||
제목이 비어 있으면 어떤 유형이든 검증을 돌릴 의미가 없다고 보고 거기서 끊습니다. 나머지 필수값
|
||||
판정은 검증 자신의 일이고, 여기서 흉내 내면 두 곳이 서로 다른 답을 낼 수 있기 때문입니다.
|
||||
|
||||
### 검증과 미리보기는 버려지지 않는 산출물이다
|
||||
|
||||
검증은 요청 값을 보는 일이 아니라 계약이 정한 체인 전체를 돌리고 그 결과를 일급 산출물로
|
||||
남깁니다. 남기는 이유는 미리보기와 게시가 `validationId` 로 「무엇을 근거로 통과했는지」를
|
||||
가리키기 때문입니다.
|
||||
|
||||
그 산출물이 아직 유효한지는 세 가지를 다 만족해야 합니다.
|
||||
|
||||
| 조건 | 무엇을 확인하나 |
|
||||
|---|---|
|
||||
| `validatedVersion == document.version` | 검증한 뒤에 문서가 바뀌지 않았는가 |
|
||||
| `dependencyRevision` 일치 | 그 문서가 참조하는 것들이 그대로인가 |
|
||||
| `now < validUntil` | 검증이 만료되지 않았는가 |
|
||||
|
||||
`dependencyRevision` 은 산출물에 박제된 값이고, 지금 계산한 값과 다르면 그 산출물은 전제가 바뀐
|
||||
것이라 더는 유효하지 않습니다.
|
||||
|
||||
미리보기는 공개 화면과 **같은** 렌더러와 Asset 리졸버를 씁니다(ADR-005). 그래야 작성자가 확인한
|
||||
화면이 공개될 화면과 같습니다.
|
||||
|
||||
### 게시는 단계마다 다른 코드로 거절한다
|
||||
|
||||
> 단계별 실패가 서로 다른 계약 코드로 나가는 것이 이 use case 의 핵심이다.
|
||||
> `DOCUMENT_VALIDATION_FAILED`(지금 검증하면 실패)와 `VALIDATION_STALE`(통과했으나 전제가 바뀜)은
|
||||
> 다른 사건이고, 작성자가 해야 할 일도 다르다 — 전자는 고치는 것이고 후자는 다시 검증하는 것이다.
|
||||
|
||||
미리보기도 같습니다 — `PREVIEW_EXPIRED`(만료됨)와 `PREVIEW_STALE`(이 버전을 설명하지 않음)이
|
||||
갈립니다. §13.4 에서 화면이 세 가지를 추측한 것이 이 설계와 정반대인 자리입니다.
|
||||
|
||||
경고를 안고 게시하려면 작성자가 그 경고 코드를 전부 확인했다고 보내야 합니다.
|
||||
|
||||
> 계약: `acknowledgedWarningCodes` 가 현재 Validation 의 WARNING 집합을 모두 덮지 못하면
|
||||
> 거절한다. 작성자가 보지 못한 경고를 안고 공개되는 일을 막는 장치다.
|
||||
|
||||
그리고 게시 시점에 다시 렌더링하지 않습니다. 사용자가 확인한 미리보기의 렌더 모델을 그대로
|
||||
씁니다 — 다시 렌더링하면 승인한 것과 다른 것이 공개될 수 있기 때문입니다.
|
||||
|
||||
공개 취소는 스냅숏을 지우지 않습니다. 과거에 무엇이 공개됐는지는 지우지 않습니다.
|
||||
|
||||
### 저장할 때와 공개할 때의 요구가 다르다
|
||||
|
||||
저장은 초안을 비워 둔 채로도 허용합니다. 아니면 한 번에 다 쓰지 않으면 저장을 못 하기
|
||||
때문입니다. 공개는 그럴 수 없습니다.
|
||||
|
||||
관계 줄도 같습니다. 대상을 아직 고르지 않은 관계 줄도 저장할 수 있고, 게시 가능한지는 검증이
|
||||
판단합니다.
|
||||
|
||||
릴리스는 이 갈림이 특히 분명합니다. `workflow_status = 'PUBLISHED'` 가 공개 조회의 유일한
|
||||
조건이므로 그 전이가 곧 공개이고, 그래서 발행 전에 계약이 필수로 선언한 것들이 실제로 채워져
|
||||
있는지 그 시점에 확인합니다.
|
||||
|
||||
### 문서가 아닌 것들은 다른 경로로 공개된다
|
||||
|
||||
프로젝트와 릴리스는 `RecordKind` 에 없습니다. 본문도 검증 대상도 없으므로 문서 게시
|
||||
파이프라인을 타지 않고 각자 자기 경로로 공개 상태를 세웁니다.
|
||||
|
||||
프로젝트는 slug 가 없으면 게시를 거절합니다.
|
||||
|
||||
> 공개 주소가 `/projects/<slug>` 이므로 slug 없이 게시하면 아무도 닿을 수 없는 페이지가 생긴다 —
|
||||
> 저장은 성공했는데 링크는 없는, 이유를 알 수 없는 상태다.
|
||||
|
||||
§4.1 에서 프로젝트가 영원히 비공개였던 것이 이 경로가 없었기 때문입니다.
|
||||
|
||||
### 참조가 있으면 지우지 않는다
|
||||
|
||||
주제를 지울 때 그 주제를 쓰는 기록이 있으면 거절합니다. 외래키를 CASCADE 로 두지 않은 이유가
|
||||
있습니다.
|
||||
|
||||
> 주제를 지웠다는 이유로 그 주제를 쓰던 문서의 분류가 조용히 사라지면 안 되기 때문이다 —
|
||||
> 지우려면 먼저 그 문서들을 옮기라는 뜻이다.
|
||||
|
||||
축은 반대입니다. 축을 지우면 걸려 있던 기록은 지우지 않고 연결만 끊고, 그 기록은 주제의 공통
|
||||
기록이 됩니다. 축이 분류가 아니라 비교 축이기 때문이고, 축 slug 제약이 `(topic_id, slug)` 쌍이라
|
||||
여러 주제가 「공통」 같은 같은 이름을 각자 가질 수 있습니다.
|
||||
|
||||
### 없는 것을 가리키는 설정을 막는다
|
||||
|
||||
홈이 무엇을 앞에 세울지 정하는 설정은 지목한 대상에 외래키를 걸지 않습니다. 설정이 대상보다
|
||||
오래 살아남는 것을 허용하는 설계입니다. 대신 저장할 때 그 대상이 실제로 있는지 확인합니다.
|
||||
|
||||
> 없는 id 를 그대로 저장할 수 있고, 그러면 공개 화면은 조용히 빈 focus 를 그린다 — 저장은
|
||||
> 성공했는데 화면에는 아무것도 안 나오는, 이유를 알 수 없는 실패가 된다.
|
||||
|
||||
대상이 *공개*인지는 확인하지 않습니다. 아직 게시하지 않은 프로젝트를 미리 지목해 두고 게시와
|
||||
동시에 홈에 뜨게 하는 것이 정상적인 순서이고, 공개 여부는 공개 조회 쪽이 매번 다시 판단합니다.
|
||||
|
||||
§10.1 의 「이 프로젝트에 열린 질문이 없습니다」가 이 설계와 같은 갈래의 반대 사례입니다 —
|
||||
저장은 성공했는데 화면이 아무것도 말하지 않는 상태를 여기서는 막고 저기서는 못 막았습니다.
|
||||
|
||||
### 서버가 판정한 것을 클라이언트가 못 바꾼다
|
||||
|
||||
Asset 상태 넷 중 둘은 서버 검증 결과라 클라이언트가 지정할 수 없습니다 — `REJECTED` 와
|
||||
`QUARANTINED` 입니다. `READY` 가 아닌 Asset 은 게시할 때 거절됩니다.
|
||||
|
||||
### 읽는 것에도 권한이 필요하다
|
||||
|
||||
Studio 표면은 조회에도 권한을 요구합니다.
|
||||
|
||||
> 읽기에도 권한이 필요한 이유: Studio 가 읽는 것은 게시 전 초안이다. 인증만 통과하면 누구나
|
||||
> `listStudioDocuments` 로 남의 초안 목록을, `getStudioDocument` 로 그 본문을 볼 수 있어서는
|
||||
> 안 된다.
|
||||
|
||||
역할 이름은 코드에 박지 않고 배포가 자기 IdP 의 역할을 권한 토큰에 잇습니다. 역할 이름이
|
||||
배포마다 다른 값이기 때문이고, 계약도 역할 이름을 고정하지 않습니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 결함을 어떻게 갈랐나
|
||||
|
||||
198개 커밋을 읽고 나서, 결함이 **원인의 종류**로 갈린다는 것이 보였습니다. 화면 증상으로 나누면
|
||||
|
||||
Reference in New Issue
Block a user