docs(TechLog): 도메인 규칙을 기록에 엮는다

§1.4 로 세운 도메인·비즈니스 규칙을 그것이 실제로 설명하는 기록에 넣었다.

  프로젝트가 문서 게시 파이프라인을 안 타는 이유 → 화면 다섯이 비어 있던 Case
  홈 focus 설정이 FK 없이 사는 설계 → 「열린 질문이 없습니다」 Case
  결정이 자기 화면을 안 갖는 이유 → 목록이 문서 전체를 실어야 했던 Case
  게시가 단계마다 다른 코드로 거절하는 설계 → 화면이 추측 셋을 출력한 Case (반대 사례)
  종류마다 애그리거트와 테이블이 다르다 → 매퍼가 종류를 판정해야 하는 Case
  축을 지우면 연결만 끊고 주제를 지우면 거절하는 이유 → 축 Concept
  개념이 문서 테이블에 얹힌다 → 열세 곳 Case
  화면 상태와 도메인 상태가 원래 갈려 있었다 → 이름을 두 번 바꾼 Case

Case 본문 중앙값 675 → 1,342 자. 검사 넷 전부 통과한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-07 19:37:13 +09:00
co-authored by Claude Opus 5
parent 4769e52e48
commit e9f6a93327
9 changed files with 79 additions and 2 deletions
@@ -68,6 +68,22 @@ topic (주제)
대가는 데이터베이스가 참조 무결성을 지켜 주지 않는다는 것이다. 기록을 지울 때 그 쌍을 함께 지우는 것은 코드가 한다. 대가는 데이터베이스가 참조 무결성을 지켜 주지 않는다는 것이다. 기록을 지울 때 그 쌍을 함께 지우는 것은 코드가 한다.
## 축을 지우면 기록은 남는다
축을 지울 때 걸려 있던 기록은 지우지 않는다. 연결만 끊고 그 기록은 주제의 공통 기록이 된다.
주제를 지울 때와 반대다. 주제는 그것을 쓰는 기록이 있으면 아예 거절한다.
> 주제를 지웠다는 이유로 그 주제를 쓰던 문서의 분류가 조용히 사라지면 안 되기 때문이다 — 지우려면 먼저 그 문서들을 옮기라는 뜻이다.
주제는 그 기록이 무엇에 관한 것인지를 말하는 분류이고, 축은 같은 주제 안에서 무엇과 견주는지를 말하는 비교 축이다. 분류가 사라지면 기록의 뜻이 바뀌고, 비교 축이 사라지면 비교만 없어진다.
## slug 제약이 주제 안에서만 걸린다
축 slug 의 유일성 제약은 주제와 slug 의 쌍이다. 그래서 다른 주제에서 같은 이름을 쓸 수 있고, 여러 주제가 「공통」 같은 같은 이름을 각자 갖는 것이 정상이다.
축 화면을 조회할 때 주제까지 함께 맞춰야 하는 이유가 이것이다. 주제를 빼면 다른 주제의 같은 이름 축이 함께 걸린다.
## 사람이 쓰는 칸 ## 사람이 쓰는 칸
주제의 논지, 축의 요약과 결론은 기록을 합쳐 자동으로 나오는 글이 아니다. 주제의 논지, 축의 요약과 결론은 기록을 합쳐 자동으로 나오는 글이 아니다.
@@ -78,6 +78,14 @@ tech-log-frontend : 계약에서 생성한 타입을 그대로 사용
다섯이 같은 원인인데 숨은 깊이가 달랐다. 프로젝트 공개 여부와 관계 연결은 화면에 자리는 있고 값만 없으므로 「아직 안 채웠다」로 읽힌다. 홈 focus 는 그보다 깊다 — 세 슬롯이 다 비면 화면이 그 영역을 통째로 그리지 않으므로, 그런 영역이 있다는 사실조차 화면에서 알 수 없다. 그래서 운영에서 한 번도 나타난 적이 없다. 다섯이 같은 원인인데 숨은 깊이가 달랐다. 프로젝트 공개 여부와 관계 연결은 화면에 자리는 있고 값만 없으므로 「아직 안 채웠다」로 읽힌다. 홈 focus 는 그보다 깊다 — 세 슬롯이 다 비면 화면이 그 영역을 통째로 그리지 않으므로, 그런 영역이 있다는 사실조차 화면에서 알 수 없다. 그래서 운영에서 한 번도 나타난 적이 없다.
## 프로젝트는 문서가 아니다
다섯 중 프로젝트 공개 여부는 결이 조금 다르다. 프로젝트는 종류 다섯에 들어 있지 않고 본문도 검증 대상도 없으므로 문서 게시 파이프라인을 타지 않는다.
> 프로젝트는 Studio 문서가 아니다 — `RecordKind` 에 없고 본문도 검증 대상도 없다. 그래서 문서 게시 파이프라인 대신 여기서 직접 공개 상태를 세운다. 릴리스가 자체 경로를 갖는 것과 같은 이유다.
공개 화면들은 전부 공개 투영의 프로젝트 행을 가시성 관문으로 쓴다. 그 행을 세우는 경로가 없었으므로 프로젝트는 영원히 비공개였고, 화면에서는 「아직 공개 안 했나 보다」로 보였다.
## 같은 계약이 반대 방향으로도 깨졌다 ## 같은 계약이 반대 방향으로도 깨졌다
빠진 구현이 화면을 비우는 것과 반대로, 있는 계약이 값을 거절하는 경우도 났다. `home_focus_config.default_focus_type` 은 마이그레이션 직후 NULL 인데 계약은 이 필드를 required 에 enum 세 값으로 선언한다. 빠진 구현이 화면을 비우는 것과 반대로, 있는 계약이 값을 거절하는 경우도 났다. `home_focus_config.default_focus_type` 은 마이그레이션 직후 NULL 인데 계약은 이 필드를 required 에 enum 세 값으로 선언한다.
@@ -73,6 +73,14 @@ tech-log-backend : 911e8ba — 없던 두 컨트롤러를 구현
읽기 전용 화면이면 방문자가 「데이터가 없나 보다」로 넘어간다. 작성 도구에서는 그 오독이 곧바로 작업 판단이 된다 — 없다고 읽으면 다시 쓰게 된다. 읽기 전용 화면이면 방문자가 「데이터가 없나 보다」로 넘어간다. 작성 도구에서는 그 오독이 곧바로 작업 판단이 된다 — 없다고 읽으면 다시 쓰게 된다.
## 이 설정은 없는 대상을 가리킬 수 있다
홈이 무엇을 앞에 세울지 정하는 설정은 지목한 대상에 외래키를 걸지 않는다. 설정이 대상보다 오래 살아남는 것을 허용하는 설계다.
대신 저장할 때 그 대상이 실제로 있는지 확인한다. 확인하지 않으면 없는 id 가 그대로 저장되고, 공개 화면은 조용히 빈 focus 를 그린다 — 저장은 성공했는데 화면에는 아무것도 안 나오는, 이유를 알 수 없는 실패가 된다.
같은 화면의 두 자리가 반대로 처리돼 있었다. 저장 쪽은 없는 대상을 막고, 목록을 못 읽은 쪽은 없는 것으로 그렸다.
## 못 읽었다고 적는다 ## 못 읽었다고 적는다
> 거짓말을 하느니 못 읽었다고 말한다. > 거짓말을 하느니 못 읽었다고 말한다.
@@ -85,6 +85,12 @@ tech-log-design-package : 76a7ccb
모양이 다른데 매퍼를 공유하면서 종류 판정까지 그쪽 것을 따랐다. 지식 목록의 매퍼는 CASE 와 REFERENCE 만 다루면 되는 화면을 위해 만들어졌으므로, 질문과 개념을 받으면 빈 값을 돌려준다. 모양이 다른데 매퍼를 공유하면서 종류 판정까지 그쪽 것을 따랐다. 지식 목록의 매퍼는 CASE 와 REFERENCE 만 다루면 되는 화면을 위해 만들어졌으므로, 질문과 개념을 받으면 빈 값을 돌려준다.
## 왜 매퍼가 종류를 판정해야 하나
편집 화면이 고르는 유형 다섯은 하나의 애그리거트가 아니다. 각 유형이 자기 애그리거트와 테이블을 갖고, 문서 세 종류(Case·Reference·Concept)만 한 테이블을 공유한다.
그래서 한 목록에 여러 종류가 섞이면 항목마다 어디서 온 것인지를 판정해야 한다. 그 판정을 다른 화면의 매퍼에서 빌려 오면 그 화면이 다루던 종류만 통과한다.
## 두 가지를 함께 고쳐야 했다 ## 두 가지를 함께 고쳐야 했다
응답 모양에 맞는 매퍼를 쓰고 모든 종류를 싣게 했다. 그것만으로는 줄이 「제목만 있고 가운뎃점만 남은」 모양이었다. 응답 모양에 맞는 매퍼를 쓰고 모든 종류를 싣게 했다. 그것만으로는 줄이 「제목만 있고 가운뎃점만 남은」 모양이었다.
@@ -80,6 +80,14 @@ const path = kind === "CASE" ? "/cases/"
CONCEPT 을 더해도 이 코드는 컴파일된다. 마지막 가지가 나머지를 받으므로 모든 값에 갈 곳이 있고, 각 가지가 내놓는 타입도 같기 때문이다. 실행하면 `/concepts/idp-brokering` 대신 프로젝트 경로가 만들어지고, 그 주소로 질문 조회가 나가 404 가 돌아온다. CONCEPT 을 더해도 이 코드는 컴파일된다. 마지막 가지가 나머지를 받으므로 모든 값에 갈 곳이 있고, 각 가지가 내놓는 타입도 같기 때문이다. 실행하면 `/concepts/idp-brokering` 대신 프로젝트 경로가 만들어지고, 그 주소로 질문 조회가 나가 404 가 돌아온다.
## 개념은 새 테이블이 아니었다
종류를 하나 더한다는 것이 테이블 하나를 더하는 일은 아니다. 편집 화면이 고르는 유형 다섯은 하나의 애그리거트가 아니고, 각 유형이 자기 애그리거트와 테이블을 갖는다.
> `CONCEPT`은 `document`의 세 번째 유형이다 — Case/Reference와 테이블을 공유하고 `concept_detail`에 기준 버전만 따로 둔다.
저장 쪽은 그래서 작았다. 늘어난 것은 그 종류를 아는 코드였고, 그 코드가 손 목록으로 흩어져 있었다.
## 열세 곳 ## 열세 곳
| # | 어디 | 증상 | 커밋 | | # | 어디 | 증상 | 커밋 |
@@ -84,6 +84,14 @@ case TOPIC_IN_USE -> "이 주제를 쓰는 기록이 있어 삭제할 수 없습
그래서 화면이 실어 나를 수 있는 것은 이 고정 문구다. 서버 안의 진단 메시지는 서버 로그에만 남는다. 그래서 화면이 실어 나를 수 있는 것은 이 고정 문구다. 서버 안의 진단 메시지는 서버 로그에만 남는다.
## 같은 저장소가 반대로 만든 경로
게시 경로는 정반대로 설계돼 있다. 단계마다 다른 계약 코드로 거절하고, 그 이유가 use case 의 javadoc 에 적혀 있다.
> 단계별 실패가 서로 다른 계약 코드로 나가는 것이 이 use case 의 핵심이다. `DOCUMENT_VALIDATION_FAILED`(지금 검증하면 실패)와 `VALIDATION_STALE`(통과했으나 전제가 바뀜)은 다른 사건이고, 작성자가 해야 할 일도 다르다 — 전자는 고치는 것이고 후자는 다시 검증하는 것이다.
미리보기도 만료된 것과 이 버전을 설명하지 않는 것이 다른 코드로 갈린다. 삭제 실패만 그 갈래를 화면에서 다시 뭉갰다.
## 무엇이 구분되지 않았나 ## 무엇이 구분되지 않았나
세 원인은 해야 할 일이 다르다. 세 원인은 해야 할 일이 다르다.
@@ -84,7 +84,7 @@ REFERENCE → 적용 기준 DECISION → 설계 결정
QUESTION → 열린 질문 QUESTION → 열린 질문
``` ```
두 안이 말하는 것은 같다. 「직접 해보니」와 「검증 기록」은 둘 다 그 글이 재현한 결과라고 말한다. 갈린 것은 이 사이트의 다른 글들이 쓰는 어조와 맞느냐다. 두 안이 말하는 것은 같다. 「직접 해보니」와 「검증 기록」은 둘 다 그 글이 재현한 결과라고 말한다. 갈린 것은 이 사이트의 다른 글들이 쓰는 어조와 맞느냐다.
## 이름 표가 하나였기 때문에 두 번 바꿀 수 있었다 ## 이름 표가 하나였기 때문에 두 번 바꿀 수 있었다
@@ -96,6 +96,19 @@ QUESTION → 열린 질문
이름을 바꿔도 이 표는 그대로였다. 표시 이름과 지식 상태가 다른 축이라 따로 두었기 때문이다. 이름을 바꿔도 이 표는 그대로였다. 표시 이름과 지식 상태가 다른 축이라 따로 두었기 때문이다.
## 화면 이름과 도메인 상태는 원래 다르다
이 저장소는 화면에 보이는 값과 도메인이 들고 있는 값을 이미 갈라 두고 있었다.
| 계약이 화면에 주는 값 | 도메인이 들고 있는 값 |
|---|---|
| 질문 상태 : OPEN · RESOLVED | OPEN · INVESTIGATING · PAUSED · RESOLVED |
| 결정 상태 : PROPOSED · ADOPTED | PROPOSED · ACCEPTED |
질문 상태 셋이 화면에서 하나로 접히고, 그 역방향 변환은 도메인 상태를 덮어쓰지 않는다. 결정의 `ACCEPTED` 는 화면에서 `ADOPTED` 로 보인다.
종류 이름을 두 번 바꾸면서 계약을 건드리지 않은 것은 새로 정한 규칙이 아니라 이미 있던 규칙을 따랐다.
## 계약의 kind 는 그대로 뒀다 ## 계약의 kind 는 그대로 뒀다
바꾼 것은 화면에 보이는 이름이다. 계약의 종류 값은 다섯 그대로이고 주소도 그대로다. 바꾼 것은 화면에 보이는 이름이다. 계약의 종류 값은 다섯 그대로이고 주소도 그대로다.
@@ -3029,5 +3029,5 @@
"unlisted": 0, "unlisted": 0,
"candidates": 91 "candidates": 91
}, },
"ssotSha256": "75e2c6368a5f6866ad72956619d020c89d4442428a79093fec29b1dfc647490c" "ssotSha256": "974abab805e33daa531fa23fde85309c0de17becf6c5e460ef5ff00d057e20c9"
} }
@@ -65,6 +65,16 @@ tech-log-frontend : 31afb4d 이후
앞선 종류들과 여기서 갈린다. Case 와 Reference 와 Question 은 상세 화면이 있어서 목록 항목이 가볍고 상세를 부르면 나머지가 온다. 결정에는 상세를 부를 곳이 없으므로 화면이 그리는 칸이 전부 목록 항목에 있어야 한다. 앞선 종류들과 여기서 갈린다. Case 와 Reference 와 Question 은 상세 화면이 있어서 목록 항목이 가볍고 상세를 부르면 나머지가 온다. 결정에는 상세를 부를 곳이 없으므로 화면이 그리는 칸이 전부 목록 항목에 있어야 한다.
## 종류마다 사는 곳이 다르다
편집 화면이 고르는 유형 다섯은 하나의 애그리거트가 아니다. 각 유형이 자기 애그리거트와 테이블을 갖고, 문서 세 종류만 한 테이블을 공유한다.
결정은 그중 문서 테이블에 살지 않는다. 프로젝트 안에서만 읽히므로 자기 목록도 자기 상세 화면도 없고, 공개 주소가 프로젝트 결정 목록 위의 앵커다.
종류별 목록 주소 표에서도 결정만 다른 규칙을 따른다. 그 표의 javadoc 이 그것을 적어 둔다.
> 결정은 프로젝트 안에서만 읽히므로 자기 목록이 없다. 그 자리를 `/projects` 로 두는 것은 빠뜨린 것이 아니라 그렇게 정한 것이고, 표에 적혀 있으니 다음 사람이 구분할 수 있다.
## 네 증상이 한 원인이었다 ## 네 증상이 한 원인이었다
목록 항목에 `title`·`summary`·`consequences`·`evidence` 가 빠져 있었고, 화면 쪽이 없는 칸을 각자 메우고 있었다. 목록 항목에 `title`·`summary`·`consequences`·`evidence` 가 빠져 있었고, 화면 쪽이 없는 칸을 각자 메우고 있었다.