diff --git a/docs/TechLog/final/document.md b/docs/TechLog/final/document.md index 0f4b17a..e5a6089 100644 --- a/docs/TechLog/final/document.md +++ b/docs/TechLog/final/document.md @@ -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 없이 게시하면 아무도 닿을 수 없는 페이지가 생긴다 — +> 저장은 성공했는데 링크는 없는, 이유를 알 수 없는 상태다. + +§4.1 에서 프로젝트가 영원히 비공개였던 것이 이 경로가 없었기 때문입니다. + +### 참조가 있으면 지우지 않는다 + +주제를 지울 때 그 주제를 쓰는 기록이 있으면 거절합니다. 외래키를 CASCADE 로 두지 않은 이유가 +있습니다. + +> 주제를 지웠다는 이유로 그 주제를 쓰던 문서의 분류가 조용히 사라지면 안 되기 때문이다 — +> 지우려면 먼저 그 문서들을 옮기라는 뜻이다. + +축은 반대입니다. 축을 지우면 걸려 있던 기록은 지우지 않고 연결만 끊고, 그 기록은 주제의 공통 +기록이 됩니다. 축이 분류가 아니라 비교 축이기 때문이고, 축 slug 제약이 `(topic_id, slug)` 쌍이라 +여러 주제가 「공통」 같은 같은 이름을 각자 가질 수 있습니다. + +### 없는 것을 가리키는 설정을 막는다 + +홈이 무엇을 앞에 세울지 정하는 설정은 지목한 대상에 외래키를 걸지 않습니다. 설정이 대상보다 +오래 살아남는 것을 허용하는 설계입니다. 대신 저장할 때 그 대상이 실제로 있는지 확인합니다. + +> 없는 id 를 그대로 저장할 수 있고, 그러면 공개 화면은 조용히 빈 focus 를 그린다 — 저장은 +> 성공했는데 화면에는 아무것도 안 나오는, 이유를 알 수 없는 실패가 된다. + +대상이 *공개*인지는 확인하지 않습니다. 아직 게시하지 않은 프로젝트를 미리 지목해 두고 게시와 +동시에 홈에 뜨게 하는 것이 정상적인 순서이고, 공개 여부는 공개 조회 쪽이 매번 다시 판단합니다. + +§10.1 의 「이 프로젝트에 열린 질문이 없습니다」가 이 설계와 같은 갈래의 반대 사례입니다 — +저장은 성공했는데 화면이 아무것도 말하지 않는 상태를 여기서는 막고 저기서는 못 막았습니다. + +### 서버가 판정한 것을 클라이언트가 못 바꾼다 + +Asset 상태 넷 중 둘은 서버 검증 결과라 클라이언트가 지정할 수 없습니다 — `REJECTED` 와 +`QUARANTINED` 입니다. `READY` 가 아닌 Asset 은 게시할 때 거절됩니다. + +### 읽는 것에도 권한이 필요하다 + +Studio 표면은 조회에도 권한을 요구합니다. + +> 읽기에도 권한이 필요한 이유: Studio 가 읽는 것은 게시 전 초안이다. 인증만 통과하면 누구나 +> `listStudioDocuments` 로 남의 초안 목록을, `getStudioDocument` 로 그 본문을 볼 수 있어서는 +> 안 된다. + +역할 이름은 코드에 박지 않고 배포가 자기 IdP 의 역할을 권한 토큰에 잇습니다. 역할 이름이 +배포마다 다른 값이기 때문이고, 계약도 역할 이름을 고정하지 않습니다. + +--- + ## 2. 결함을 어떻게 갈랐나 198개 커밋을 읽고 나서, 결함이 **원인의 종류**로 갈린다는 것이 보였습니다. 화면 증상으로 나누면