§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>
124 lines
7.6 KiB
Markdown
124 lines
7.6 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: five-screens-were-quietly-empty
|
|
title: 계약에 선언만 있고 구현이 없어 화면 다섯 곳이 비어 있었다
|
|
topic: declared-but-not-implemented
|
|
topicName: 계약에 선언만 있고 구현이 없다
|
|
project: TechLog
|
|
status: 게시 전
|
|
sourceRevision: tech-log@2026-09-02
|
|
source:
|
|
- final/document.md#§4.1
|
|
- final/document.md#§4.2
|
|
- final/document.md#§10.4
|
|
---
|
|
|
|
# 계약에 선언만 있고 구현이 없어 화면 다섯 곳이 비어 있었다
|
|
|
|
홈의 「지금 집중하는 것」 영역은 화면에 나타난 적이 없었고, 프로젝트는 공개할 방법이 없었고, 어떤 기록도 다른 기록을 연결 대상으로 고를 수 없었다. 계약에는 그 연산들이 전부 선언돼 있었다. 서버에 구현이 없었고, 프론트는 계약을 믿고 불렀고, 화면은 404 를 「데이터가 없음」으로 그렸다.
|
|
|
|
## 관계
|
|
|
|
- **타입에는 보이는데 부를 수 없는 연산이 네 번 나왔다**
|
|
같은 시기에 난 다른 부류의 누락이다. 이쪽은 서버에 구현이 없었고 그쪽은 프론트가 등록을 빠뜨렸다.
|
|
- **계약과 구현은 서버와 화면 양쪽에서 전수 대조한다**
|
|
이 사건 뒤에 세운 기준이다.
|
|
- **「이 프로젝트에 열린 질문이 없습니다」 — 실제로는 넷이 있었다**
|
|
같은 404 를 화면이 어떻게 그렸는지가 그 기록에 있다.
|
|
|
|
## 문제
|
|
|
|
계약이 「이 연산이 있다」고 말하면 프론트는 그것을 부른다. 서버에 그 컨트롤러가 없으면 404 가 돌아오고, 화면은 그 404 를 빈 목록으로 그린다.
|
|
|
|
빈 목록과 「아직 안 쓴 글」은 화면에서 같아 보인다. 그래서 다섯 화면이 비어 있는 동안 아무도 오류를 보지 못했다.
|
|
|
|
## 결론
|
|
|
|
다섯 화면이 비어 있었고 원인은 하나였다. 계약에 선언만 있고 구현이 없었다.
|
|
|
|
홈 「지금 집중하는 것」 : 세 슬롯이 다 비면 영역 자체를 그리지 않아 운영에서 나타난 적이 없다
|
|
프로젝트 공개 여부 : 투영의 PROJECT 행을 세우는 경로가 없어 영원히 비공개였다
|
|
문서 사이 관계 연결 : 어댑터의 RELATION 과 EVIDENCE 가 빈 목록 스텁이었다
|
|
프로젝트 활동 : 목록·생성·수정이 계약에 있고 테이블은 0행이었다
|
|
릴리즈 : 읽는 쪽만 있고 쓰는 쪽이 없었다
|
|
|
|
생성 모델 검사는 schema 와 property 만 보므로 이 구멍을 잡지 못한다. 모델은 멀쩡히 생성된다.
|
|
|
|
## 검증 환경
|
|
|
|
tech-log-backend : 365560e 이후
|
|
tech-log-frontend : 계약에서 생성한 타입을 그대로 사용
|
|
확인 방식 : 계약이 선언한 연산과 `@RestController` 매핑을 리플렉션으로 대조
|
|
|
|
## 재현 조건
|
|
|
|
1. 계약에 연산을 하나 선언하고 컨트롤러는 만들지 않는다
|
|
2. 프론트에서 그 연산을 부르는 화면을 연다 — 404 가 돌아오고 화면은 빈 목록을 그린다
|
|
3. `ContractRouteCoverageTest` 를 돌린다 — 그 연산 하나를 짚는다
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
## 빈 화면과 안 쓴 글이 같아 보인다
|
|
|
|
계약이 「이 연산이 있다」고 말하면 프론트는 그것을 부른다. 서버에 그 컨트롤러가 없으면 404 가 돌아오고, 화면은 목록 요청이 실패했을 때와 0건일 때를 같은 그림으로 그린다.
|
|
|
|
작성 도구에서 이것이 특히 오래 숨는다. 작성자는 자기가 쓴 것과 화면을 대조하므로, 「없습니다」를 보면 아직 안 썼거나 게시하지 않았다고 읽는다.
|
|
|
|
## 비어 있던 다섯 화면
|
|
|
|
| 무엇이 비었나 | 왜 |
|
|
|---|---|
|
|
| 홈 「지금 집중하는 것」 | `home_focus_config` 는 마이그레이션이 빈 행 하나만 넣었고, `getHomeFocus`/`updateHomeFocus` 는 구현이 없었다 |
|
|
| 프로젝트 공개 여부 | 프로젝트는 `RecordKind` 에 없어 문서 게시 파이프라인을 타지 못하는데, 공개 화면들은 전부 `public_resource_projection` 의 PROJECT 행을 가시성 관문으로 쓴다 |
|
|
| 문서 사이 관계 연결 | `JdbcCatalogQueryAdapter` 의 RELATION/EVIDENCE 가 「슬라이스 2·5에서 채운다」는 주석과 함께 `List.of()` 스텁이었다 |
|
|
| 프로젝트 활동 | 계약에 목록·생성·수정이 선언돼 있었지만 구현이 없었고 `project_activity` 는 0행이었다 |
|
|
| 릴리즈(변경 기록) | 읽는 쪽은 있는데 쓰는 쪽이 없어, 페이지는 영원히 빈 채였다 |
|
|
|
|
다섯이 같은 원인인데 숨은 깊이가 달랐다. 프로젝트 공개 여부와 관계 연결은 화면에 자리는 있고 값만 없으므로 「아직 안 채웠다」로 읽힌다. 홈 focus 는 그보다 깊다 — 세 슬롯이 다 비면 화면이 그 영역을 통째로 그리지 않으므로, 그런 영역이 있다는 사실조차 화면에서 알 수 없다. 그래서 운영에서 한 번도 나타난 적이 없다.
|
|
|
|
## 프로젝트는 문서가 아니다
|
|
|
|
다섯 중 프로젝트 공개 여부는 결이 조금 다르다. 프로젝트는 종류 다섯에 들어 있지 않고 본문도 검증 대상도 없으므로 문서 게시 파이프라인을 타지 않는다.
|
|
|
|
> 프로젝트는 Studio 문서가 아니다 — `RecordKind` 에 없고 본문도 검증 대상도 없다. 그래서 문서 게시 파이프라인 대신 여기서 직접 공개 상태를 세운다. 릴리스가 자체 경로를 갖는 것과 같은 이유다.
|
|
|
|
공개 화면들은 전부 공개 투영의 프로젝트 행을 가시성 관문으로 쓴다. 그 행을 세우는 경로가 없었으므로 프로젝트는 영원히 비공개였고, 화면에서는 「아직 공개 안 했나 보다」로 보였다.
|
|
|
|
## 같은 계약이 반대 방향으로도 깨졌다
|
|
|
|
빠진 구현이 화면을 비우는 것과 반대로, 있는 계약이 값을 거절하는 경우도 났다. `home_focus_config.default_focus_type` 은 마이그레이션 직후 NULL 인데 계약은 이 필드를 required 에 enum 세 값으로 선언한다.
|
|
|
|
배포 직후 첫 요청부터 `/home` 이 깨졌다. `HomeFocusView.resolve` 가 반드시 유효한 값 하나를 정하도록 고쳤다.
|
|
|
|
## 생성 모델 검사가 못 잡는 이유
|
|
|
|
계약에서 모델을 생성하는 단계는 스키마와 속성만 본다. 연산에 구현이 없어도 그 연산의 요청·응답 모델은 멀쩡히 만들어지고 컴파일도 통과한다. 그래서 「모델이 생성됐다」는 구현이 있다는 증거가 아니다.
|
|
|
|
## 계약과 컨트롤러를 전수로 맞춘다
|
|
|
|
`ContractRouteCoverageTest` 가 `@RestController` 들을 리플렉션으로 훑어 매핑을 모으고 계약이 선언한 경로와 대조한다. 기대 목록을 손으로 적지 않고 계약에서 읽으므로, 연산을 더하고 컨트롤러를 잊으면 여기서 멈춘다.
|
|
|
|
면제는 상수 둘로 명시한다.
|
|
|
|
```java
|
|
private static final Set<String> ELSEWHERE = Set.of("getPublicMedia");
|
|
private static final Set<String> SUPERSEDED_BY_WORKING_COPY_API =
|
|
Set.of(
|
|
"acceptProjectDecision",
|
|
"addQuestionUpdate",
|
|
"archiveCase",
|
|
…);
|
|
```
|
|
|
|
작업본 API 로 대체된 옛 연산 51개가 뒤쪽 목록에 있다. 이것을 적어 두지 않으면 대조 결과가 51건의 실패로 나오고, 그렇게 되면 아무도 결과를 읽지 않는다. 봉투 없이 바이트를 주는 미디어 연산 하나만 앞쪽 목록으로 면제한다.
|
|
|
|
매핑을 떼어 보고 그 연산 하나를 정확히 짚는 것을 확인했다. 프론트에도 같은 가드를 뒀다 — 양쪽에서 봐야 한쪽만 지웠을 때 잡힌다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
홈 focus 의 옛 증상은 재현할 수 없다. 세 슬롯이 다 비면 영역을 그리지 않으므로 화면에 남은 흔적이 없고, 지금 고쳐져 있다는 것만 확인했다.
|
|
|
|
<!-- body:end -->
|