docs(TechLog): 얇은 Case 열 편과 Question 셋을 저장소 실물로 채운다
SSOT 를 저장소에서 확인해 더 보강하고 그것으로 다시 썼다.
§7.2 참조 검사 SQL 을 문자열로 조립하는 실제 코드 — 컴파일러가 표 이름도
컬럼 이름도 보지 않는다는 것이 그 모양에서 드러난다
§11.2 section-heading-rank 가 미디어 쿼리 값을 먼저 걷어내는 이유(테스트 주석)
§16.1 질문 삭제는 참조가 둘뿐이라 같은 문제가 덜하다는 대조
Case 열 편과 Question 셋을 다시 썼다. Question 은 사실·가정·미지수·제약을 갈라
채우고 선택지마다 무엇을 감수하는지 적었다 — 오류 코드를 나누면 계약과 반입한 두
저장소가 함께 움직인다는 것처럼.
SSOT 62,643 → 68,319 자. 검사 넷 전부 통과한다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6917ce2420
commit
fd221353a3
@@ -548,6 +548,19 @@ refresh 에 실패해 **파드가 CrashLoopBackOff** 로 들어갔습니다.
|
||||
|
||||
> 그 쿼리의 여섯 컬럼 중 다섯은 마이그레이션과 대조했다. 이 하나만 가정했고, 그것이 틀렸다.
|
||||
|
||||
그 어댑터는 SQL 을 문자열로 이어 붙여 만듭니다. 컴파일러가 확인하는 것은 이 식이 문자열이라는
|
||||
것까지이고, 표 이름도 컬럼 이름도 실행해야 검증됩니다.
|
||||
|
||||
```java
|
||||
"SELECT EXISTS ("
|
||||
+ " SELECT 1 FROM document_relation WHERE target_document_id = :id"
|
||||
+ " UNION ALL SELECT 1 FROM question_document_link WHERE document_id = :id"
|
||||
+ " UNION ALL SELECT 1 FROM project_document_link WHERE document_id = :id"
|
||||
+ " UNION ALL SELECT 1 FROM topic_featured_document WHERE document_id = :id"
|
||||
+ " UNION ALL SELECT 1 FROM project_decision WHERE source_case_id = :id"
|
||||
+ ")"
|
||||
```
|
||||
|
||||
**진짜 실패는 이 SQL 이 한 번도 실행된 적이 없다는 것이었습니다.** 표준 `check` 는
|
||||
Testcontainers 를 띄우지 않으므로 **persistence SQL 은 한 번도 실행되지 않은 채 빌드가
|
||||
통과합니다.** 컴파일도 단위 테스트도 컬럼 이름을 검증하지 못합니다.
|
||||
@@ -915,6 +928,11 @@ Studio 에 주제 설명을 쓸 칸조차 없었기 때문입니다.
|
||||
`font-size`/`font-weight` 를 쓰는지 CSS 를 파싱해 확인합니다. 굵기를 빼 보고 실제로 멈추는
|
||||
것을 확인했습니다.
|
||||
|
||||
이 검사는 미디어 쿼리 안의 값을 먼저 걷어냅니다. 그 이유가 테스트 안에 적혀 있습니다:
|
||||
|
||||
> 좁은 화면용 값은 뺀다. 화면 폭마다 급이 달라지는 것은 의도이고, 여기서 보려는 것은 같은
|
||||
> 폭에서 나란히 설 때의 급이다.
|
||||
|
||||
> **이때 제가 저지른 판단 오류:** 처음에 grid/columns 만 측정하고 "정상"이라고 답했습니다.
|
||||
> 사용자가 다시 지적한 뒤 **전체 페이지 스크린샷**을 찍어서야 26px/400 을 봤습니다.
|
||||
> **프록시 지표가 아니라 보이는 것을 측정해야 합니다.**
|
||||
@@ -1358,6 +1376,10 @@ UNION ALL SELECT 1 FROM topic_featured_document WHERE document_id = :id
|
||||
UNION ALL SELECT 1 FROM project_decision WHERE source_case_id = :id
|
||||
```
|
||||
|
||||
같은 어댑터의 질문 삭제는 참조가 둘뿐이라 같은 문제가 덜합니다 — `project_question_link` 와
|
||||
`home_focus_config.open_question_id` 입니다. 다섯을 하나로 묶은 쪽에서만 어느 것이 막았는지가
|
||||
사라집니다.
|
||||
|
||||
실제 사례: 「DTO 변환 과정에서 발생한 Highlight 컬렉션 N+1」(CASE, DRAFT/PRIVATE)을 지우려는데
|
||||
관계를 다 지워도 삭제가 안 됐습니다. 남아 있던 것은 `project_document_link` 의 **PRIMARY 링크
|
||||
1행**(프로젝트 「Liner N + 1문제」)이었습니다. **프로젝트 연결은 「관계」 편집기가 아니라 문서의
|
||||
|
||||
+7
-1
@@ -68,6 +68,12 @@ tech-log-design-package : 71bab4c · b93d62a
|
||||
|
||||
주제 화면에서 그 링크를 누르면 주소에 앵커가 붙는다. 화면은 이미 그 주제 화면이므로 라우터가 다시 그릴 것이 없고, 앵커가 가리키는 element 도 없으니 스크롤도 일어나지 않는다.
|
||||
|
||||
## 앵커로 옮긴 것이 왜 나쁘지 않았나
|
||||
|
||||
앵커는 결정에는 맞는 선택이다. 결정은 프로젝트 화면 안에서만 읽히므로 자기 화면이 필요 없고, 목록 위의 앵커로 충분하다.
|
||||
|
||||
축은 다르다. 축을 고르면 그 축에 걸린 기록만 보여야 하는데, 앵커는 화면을 바꾸지 않고 위치만 옮긴다. 같은 표기를 쓰지만 두 경우가 요구하는 것이 다르다.
|
||||
|
||||
## 축에 자기 화면을 줬다
|
||||
|
||||
목록 조회에 축 필터를 더하고 기록이 어느 축에 걸리는지를 담은 표로 거른다.
|
||||
@@ -84,7 +90,7 @@ tech-log-design-package : 71bab4c · b93d62a
|
||||
|
||||
같은 시기에 주제 없이 게시된 기록이 이름 없는 주제 링크를 달고 있었다. 문서 머리말의 breadcrumb 과 탐색의 「주제 없음」 묶음 둘 다였다.
|
||||
|
||||
프로젝트 조각은 처음부터 조건부였다. 주제 쪽만 조건이 없었고, 그 차이가 두 화면에서 같은 모양으로 나타났다.
|
||||
프로젝트 조각은 처음부터 조건부였다. 주제 쪽만 조건이 없었고, 그 차이가 두 화면에서 같은 모양으로 나타났다 — 값이 없을 때 그 조각을 그리지 않는 판단을 한쪽에만 넣었다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
|
||||
+8
-6
@@ -30,19 +30,21 @@ source:
|
||||
|
||||
## 사실
|
||||
|
||||
홈의 비교 구역에서 한 줄은 축 하나다. 기록을 스무 개 붙여도 줄 수는 늘지 않는다.
|
||||
홈의 비교 구역에서 한 줄은 축 하나다. 기록을 스무 개 붙여도 줄 수는 그대로다.
|
||||
|
||||
줄에 보이는 결론 문장은 축에 손으로 쓴 글이다. 기록을 붙여도 그 문장은 누가 고치기 전까지 바뀌지 않는다.
|
||||
줄에 보이는 결론 문장은 축에 손으로 쓴 글이다. 기록을 붙여도 그 문장은 누가 고치기 전까지 그대로다.
|
||||
|
||||
주제 화면에는 축마다 「기록 N」이 붙는다. 홈에는 그 수가 없다.
|
||||
|
||||
그래서 기록 1개짜리 축과 20개짜리 축이 홈에서 똑같아 보인다.
|
||||
|
||||
홈이 처음 보내는 요청은 목록 하나와 주제 하나로 고정돼 있다. 상세는 고른 탭만 그때 받아 캐시한다.
|
||||
|
||||
주제의 논지와 축의 결론은 2026-09-01 에 AI 가 써서 DB 에 직접 넣은 초안이다. 사용자 검토 대상이고 아직 검토되지 않았다.
|
||||
|
||||
## 가정
|
||||
|
||||
결론을 사람이 쓰게 한 것이 의도라고 보고 있다. 요약은 「무엇인가」이고 결론은 「무엇을 알게 됐나」라서 기록의 요약 첫 줄을 잘라 쓰면 안 된다고 판단했지만, 그 판단이 지금도 맞는지 다시 보지 않았다.
|
||||
결론을 사람이 쓰게 한 것이 의도라고 보고 있다. 요약은 「무엇인가」이고 결론은 「무엇을 알게 됐나」라서 기록의 요약 첫 줄을 잘라 쓰면 안 된다고 판단했는데, 그 판단이 지금도 맞는지 다시 보지 않았다.
|
||||
|
||||
기록 수는 유도할 수 있다고 보고 있다. 주제 화면이 이미 그 수를 그리므로 같은 값을 홈에 붙이면 된다고 짐작하지만, 홈의 목록 호출이 그 수를 싣는지 확인하지 않았다.
|
||||
|
||||
@@ -50,7 +52,7 @@ source:
|
||||
|
||||
홈에 기록 수를 붙이면 축에 기록을 더했을 때 화면이 달라지는가. 달라진다면 결론 문장이 낡았다는 것도 같은 화면에서 드러나는가.
|
||||
|
||||
결론을 사람이 갱신해야 한다는 것을 화면이 말해야 하는가. 말한다면 어디에 말해야 읽히는가.
|
||||
결론을 사람이 갱신해야 한다는 것을 화면이 말해야 하는가. 말한다면 어디에 말해야 읽히는가 — 공개 화면에 적으면 독자가 그것을 읽고, Studio 에만 적으면 작성자만 읽는다.
|
||||
|
||||
DB 에 직접 넣은 초안을 누가 언제 검토하는가. 검토 전까지 그 문장을 공개 화면에 그대로 둘지.
|
||||
|
||||
@@ -63,10 +65,10 @@ DB 에 직접 넣은 초안을 누가 언제 검토하는가. 검토 전까지
|
||||
## 선택지
|
||||
|
||||
**홈 비교표에 기록 수를 붙인다**
|
||||
목록 호출이 이미 그 수를 실을 수 있으면 요청이 늘지 않는다. 결론 문장은 여전히 사람이 쓴다.
|
||||
목록 호출이 이미 그 수를 실을 수 있으면 요청이 늘지 않는다. 결론 문장은 그대로 사람이 쓴다. 기록이 늘면 수가 바뀌므로 결론만 낡은 상태가 화면에 드러난다.
|
||||
|
||||
**결론 문장이 마지막으로 고쳐진 때를 함께 보인다**
|
||||
기록이 그 뒤에 늘었으면 낡았다는 것이 드러난다. 화면에 날짜가 하나 더 늘어난다.
|
||||
기록이 그 뒤에 늘었으면 낡았다는 것이 드러난다. 화면에 날짜가 하나 더 늘고, 독자에게는 필요 없는 값이 보인다.
|
||||
|
||||
**결론을 쓰지 않은 축은 결론 줄을 비운다**
|
||||
쓰지 않은 것과 낡은 것을 구분한다. 지금은 초안이 들어 있어 둘이 같아 보인다.
|
||||
|
||||
+9
-3
@@ -66,7 +66,7 @@ tech-log-frontend : 68538f2
|
||||
| 크기 | 30px | 26px (각자 선언) |
|
||||
| 굵기 | 650 | 400 (아무도 선언하지 않음) |
|
||||
|
||||
같은 화면에 나란히 서 있는 구역들이라 이 둘만 급이 낮아 보였다.
|
||||
같은 화면에 나란히 서 있는 구역들이라 이 둘만 급이 낮아 보였다. 「구조별로 알게 된 것」이 형제 구역보다 작고 가늘게 나왔고, 보고는 「디자인이 안 됐다」로 왔다.
|
||||
|
||||
## 처음에 잘못 판단한 것
|
||||
|
||||
@@ -74,9 +74,15 @@ tech-log-frontend : 68538f2
|
||||
|
||||
격자와 열은 정상이었다. 보고된 증상은 글자 크기와 굵기였는데 다른 축을 재고 답했다.
|
||||
|
||||
## 검사
|
||||
## 검사가 무엇을 보고 무엇을 빼나
|
||||
|
||||
`section-heading-rank.test.ts` 가 나란히 서는 구역 제목들이 정본과 같은 `font-size` 와 `font-weight` 를 쓰는지 CSS 를 파싱해 확인한다.
|
||||
`section-heading-rank.test.ts` 가 globals.css 를 읽어 나란히 서는 구역 제목들이 정본과 같은 `font-size` 와 `font-weight` 를 쓰는지 확인한다.
|
||||
|
||||
이 검사는 미디어 쿼리 안의 값을 먼저 걷어낸다. 그 이유가 테스트 안에 적혀 있다.
|
||||
|
||||
> 좁은 화면용 값은 뺀다. 화면 폭마다 급이 달라지는 것은 의도이고, 여기서 보려는 것은 같은 폭에서 나란히 설 때의 급이다.
|
||||
|
||||
걷어내지 않으면 좁은 화면에서 일부러 급을 낮춘 선언이 전부 위반으로 나온다. 그러면 결과가 늘 빨갛고 곧 읽히지 않는다.
|
||||
|
||||
굵기를 빼 보고 실제로 멈추는 것을 확인한 뒤 커밋했다. 확인하지 않았다면 그 구역들이 원래 정본을 쓰고 있어서 통과한 것인지 검사가 그 구역을 안 보는 것인지 구별되지 않는다.
|
||||
|
||||
|
||||
+6
-4
@@ -61,7 +61,7 @@ tech-log-backend : 911e8ba — 없던 두 컨트롤러를 구현
|
||||
|
||||
편집기는 질문 목록을 받아 고를 수 있게 그린다. 목록이 비면 「이 프로젝트에 열린 질문이 없습니다」를 적는다.
|
||||
|
||||
요청이 실패했을 때도 빈 배열이 되고 있었다. 그래서 「없다」와 「못 읽었다」가 같은 화면이 됐다.
|
||||
요청이 실패했을 때도 빈 배열이 되고 있었다. 실패 경로가 빈 값을 만들고, 그 아래의 「비어 있으면 이 문구」 분기가 두 경우를 같은 화면으로 만든다.
|
||||
|
||||
서버는 404 를 주고 있었다. 그 두 목록 조회에 컨트롤러가 없었고, 계약에는 선언돼 있어 프론트가 그것을 믿고 불렀다.
|
||||
|
||||
@@ -69,9 +69,9 @@ tech-log-backend : 911e8ba — 없던 두 컨트롤러를 구현
|
||||
|
||||
작성자는 자기가 쓴 것과 화면을 대조한다. 화면이 「없습니다」라고 하면 아직 안 썼거나 게시하지 않았다고 읽는다.
|
||||
|
||||
이 경우에는 넷이 있었고 공개 사이트에도 나오고 있었다. 두 화면이 같은 데이터베이스를 보는데 한쪽만 비어 있었으므로, 공개 사이트를 함께 보지 않으면 알아챌 방법이 없었다.
|
||||
이 경우에는 넷이 있었고 공개 사이트에도 나오고 있었다. 두 화면이 같은 데이터베이스를 보는데 한쪽만 비어 있었으므로, 공개 사이트를 함께 열어 보지 않으면 알아챌 방법이 없었다.
|
||||
|
||||
작성 도구에서 이 부류가 특히 오래 숨는다. 읽기 전용 화면이면 방문자가 「데이터가 없나 보다」로 넘어가지만, 작성 도구에서는 그 오독이 곧바로 작업 판단이 된다 — 없다고 읽으면 다시 쓰게 된다.
|
||||
읽기 전용 화면이면 방문자가 「데이터가 없나 보다」로 넘어간다. 작성 도구에서는 그 오독이 곧바로 작업 판단이 된다 — 없다고 읽으면 다시 쓰게 된다.
|
||||
|
||||
## 못 읽었다고 적는다
|
||||
|
||||
@@ -79,11 +79,13 @@ tech-log-backend : 911e8ba — 없던 두 컨트롤러를 구현
|
||||
|
||||
요청이 실패하면 실패했다고 적고 0건은 0건이라고 적는다. 이 둘을 구분할 수 있어야 작성자가 다음에 무엇을 할지 정한다 — 실패면 다시 부르거나 서버를 보고, 0건이면 쓰면 된다.
|
||||
|
||||
실패를 빈 값으로 접는 지점을 없애는 것이 고치는 방법이다. 문구만 바꾸면 그 지점이 그대로여서 다음 화면에서 같은 일이 난다.
|
||||
|
||||
## 같은 판단을 다른 화면에
|
||||
|
||||
주제 탭에도 같은 판단을 적용했다. 탭 하나를 못 받아도 탭 줄과 나머지 탭은 그대로 그리고, 못 받은 탭에는 못 받았다고 적는다.
|
||||
|
||||
탭 줄이 남는 것이 중요하다. 탭 줄까지 사라지면 그 주제가 없는 것처럼 보이고, 그러면 못 읽은 범위가 화면에서 더 넓어진다.
|
||||
탭 줄이 남는 것이 중요하다. 탭 줄까지 사라지면 그 주제가 없는 것처럼 보이고, 못 읽은 범위가 화면에서 더 넓어진다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
|
||||
+7
-1
@@ -70,10 +70,16 @@ tech-log-frontend : 6e784ed · fd73bc8 · 3bb724b
|
||||
|
||||
> `Promise.all([gateway.foo()])` 은 foo 가 **거절하는 것만** 잡는다. 호출이 **동기적으로 던지면** 배열을 만드는 중에 터져 rejection handler 를 지나지 못하고, 그러면 홈 focus 한 칸 때문에 대시보드 전체가 빈 화면이 된다.
|
||||
|
||||
배열 리터럴 안에서 함수를 부르는 시점은 `Promise.all` 에 배열이 넘어가기 전이다. 그 호출이 동기적으로 던지면 배열이 완성되지 않으므로 `Promise.all` 이 시작조차 하지 않고, 거기에 붙인 거절 처리기도 붙을 대상이 없다.
|
||||
배열 리터럴 안에서 함수를 부르는 시점은 그 배열이 `Promise.all` 에 넘어가기 전이다. 그 호출이 동기적으로 던지면 배열이 완성되지 않으므로 `Promise.all` 이 시작조차 하지 않고, 거기에 붙인 거절 처리기도 붙을 대상이 없다.
|
||||
|
||||
거절만 잡는 처리로는 부족하다. 던지는 경로도 함께 잡아야 한 칸의 실패가 화면 전체로 번지지 않는다.
|
||||
|
||||
## 왜 동기적으로 던질 수 있나
|
||||
|
||||
게이트웨이 호출이 비동기 함수여도 그 안의 첫 줄이 동기적으로 실행된다. 인자를 검증하거나 연산을 고르는 코드가 거기 있고, 등록되지 않은 연산을 고르면 거기서 바로 던진다.
|
||||
|
||||
그래서 「비동기 함수를 불렀으니 거절로 온다」는 전제가 성립하지 않는다.
|
||||
|
||||
## 탭에도 같은 판단을
|
||||
|
||||
탭 줄은 목록 호출 하나가 주는 전부이고 상세는 고른 탭만 그때 받는다. 상세 하나를 못 받아도 탭 줄과 나머지 탭은 그대로 그리고, 못 받은 탭에는 못 받았다고 적는다.
|
||||
|
||||
+13
-8
@@ -62,7 +62,7 @@ tech-log-design-package : 76a7ccb
|
||||
|
||||
## null 을 돌려주고 걸러 내면 흔적이 없다
|
||||
|
||||
매퍼가 아는 종류가 아니면 `null` 을 돌려주고, 호출부는 그 목록에서 `null` 을 걸러 낸다.
|
||||
매퍼가 아는 종류가 아니면 빈 값을 돌려주고, 호출부는 그 목록에서 빈 값을 걸러 낸다.
|
||||
|
||||
이 조합에서는 오류가 나지 않고 빈 줄도 생기지 않는다. 목록의 길이만 줄어든다.
|
||||
|
||||
@@ -72,22 +72,27 @@ tech-log-design-package : 76a7ccb
|
||||
| 화면의 빈 줄 | x |
|
||||
| 목록 길이의 변화 | o |
|
||||
|
||||
목록에 몇 개가 있어야 하는지 아는 사람만 알아챌 수 있고, 공개 화면에서는 그것을 아는 사람이 작성자뿐이다.
|
||||
목록에 몇 개가 있어야 하는지 아는 사람만 알아챌 수 있다. 공개 화면에서는 그것을 아는 사람이 작성자뿐이고, 작성자는 그 목록을 자기 기록의 전부로 읽는다.
|
||||
|
||||
## 응답 모양이 다른 목록에 다른 매퍼를 썼다
|
||||
|
||||
프로젝트 기록 목록과 탐색의 지식 목록은 응답 모양이 다르다. 프로젝트 쪽은 관계 항목을 그대로 실어 요약도 주제도 게시일도 없었고, 지식 목록은 처음부터 그 칸들을 갖고 있었다.
|
||||
프로젝트 기록 목록과 탐색의 지식 목록은 응답 모양이 다르다.
|
||||
|
||||
모양이 다른데 매퍼를 공유하면서 종류 판정까지 그쪽 것을 따랐다. 지식 목록의 매퍼는 CASE 와 REFERENCE 만 다루면 되는 화면을 위해 만들어졌으므로, 질문과 개념을 받으면 `null` 을 돌려준다.
|
||||
| | 프로젝트 기록 목록 | 탐색의 지식 목록 |
|
||||
|---|---|---|
|
||||
| 무엇을 실었나 | 관계 항목 그대로 | 요약·주제·게시일까지 |
|
||||
| 아는 종류 | (매퍼를 빌림) | CASE · REFERENCE |
|
||||
|
||||
## 고친 것
|
||||
모양이 다른데 매퍼를 공유하면서 종류 판정까지 그쪽 것을 따랐다. 지식 목록의 매퍼는 CASE 와 REFERENCE 만 다루면 되는 화면을 위해 만들어졌으므로, 질문과 개념을 받으면 빈 값을 돌려준다.
|
||||
|
||||
응답 모양에 맞는 매퍼를 쓰고 모든 종류를 싣게 했다. 목록 항목에 요약과 주제와 게시일을 더해 「제목만 있고 가운뎃점만 남은」 줄을 없앴다.
|
||||
## 두 가지를 함께 고쳐야 했다
|
||||
|
||||
이 목록은 계약도 함께 고쳐야 했다. 관계 항목을 그대로 싣던 구조라 요약·주제·게시일을 담을 칸이 응답에 없었다.
|
||||
응답 모양에 맞는 매퍼를 쓰고 모든 종류를 싣게 했다. 그것만으로는 줄이 「제목만 있고 가운뎃점만 남은」 모양이었다.
|
||||
|
||||
목록 항목이 관계 항목을 그대로 싣던 구조라 요약도 주제도 게시일도 담을 칸이 응답에 없었기 때문이다. 계약에 그 칸들을 더하고 백엔드가 그것을 채우게 한 뒤에야 줄이 완성됐다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
`null` 을 돌려주고 호출부가 거르는 매퍼가 다른 목록에도 남아 있는지는 세지 않았다.
|
||||
빈 값을 돌려주고 호출부가 거르는 매퍼가 다른 목록에도 남아 있는지는 세지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
|
||||
+10
-6
@@ -30,11 +30,15 @@ CI 게이트가 설치된 라우트마다 수동 접근성 증거 파일을 하
|
||||
|
||||
FE-GATE-009 는 설치된 라우트마다 증거 파일 하나를 요구하고, 그 집합이 정확히 일치하지 않으면 거절한다.
|
||||
|
||||
정확한 일치를 요구하는 이유는 빠뜨림이 통과가 되지 않게 하려는 것이다. 파일이 더 많아도 더 적어도 거절한다.
|
||||
|
||||
`artifacts/tests/a11y-manual/*.md` 는 전부 `pending-manual-review` 다.
|
||||
|
||||
`review:a11y-manual` 스크립트는 그래서 실패하는 것이 지금 정상이다.
|
||||
`review:a11y-manual` 스크립트는 그래서 실패하는 것이 지금은 정상이다.
|
||||
|
||||
라우트를 더할 때마다 이 증거 개수도 늘어, 커밋 넷에서 111 → 117 이 됐다.
|
||||
라우트를 더할 때마다 이 증거 개수가 함께 움직였고, 커밋 넷에서 111 → 117 로 늘었다.
|
||||
|
||||
게이트가 세는 것은 파일의 존재이고 그 안의 서명 상태는 보지 않는다.
|
||||
|
||||
## 가정
|
||||
|
||||
@@ -46,13 +50,13 @@ FE-GATE-009 는 설치된 라우트마다 증거 파일 하나를 요구하고,
|
||||
|
||||
게이트가 서명 여부까지 보게 하면 지금 몇 개의 라우트가 막히는가.
|
||||
|
||||
서명을 요구하지 않기로 한다면 이 게이트가 파일 개수를 세는 것이 무엇을 막는가.
|
||||
서명을 요구하지 않기로 한다면 이 게이트가 파일 개수를 세는 것이 무엇을 막는가. 파일이 있고 내용이 비어 있으면 지금도 통과한다.
|
||||
|
||||
라우트 하나를 사람이 실제로 검토하는 데 얼마가 드는가.
|
||||
라우트 하나를 사람이 실제로 검토하는 데 얼마가 드는가. 그 비용을 모르면 어느 쪽도 고를 수 없다.
|
||||
|
||||
## 제약
|
||||
|
||||
FE-GATE-009 는 라우트 집합과 증거 집합이 정확히 일치하기를 요구한다. 이 규칙은 바꾸지 않는다.
|
||||
FE-GATE-009 는 라우트 집합과 증거 집합이 정확히 일치하기를 요구한다. 이 규칙은 바꾸지 않는다 — 빠뜨림이 통과가 되면 게이트가 아니다.
|
||||
|
||||
수동 접근성 증거는 사람이 만든다. 자동 검사로 대신하지 않는다.
|
||||
|
||||
@@ -65,7 +69,7 @@ FE-GATE-009 는 라우트 집합과 증거 집합이 정확히 일치하기를
|
||||
게이트가 세는 것이 무엇인지 분명해진다. 수동 검토를 하지 않기로 하는 결정이므로 그 결과를 따로 적어야 한다.
|
||||
|
||||
**게이트는 그대로 두고 서명 현황을 별도로 보고한다**
|
||||
배포는 막지 않고 서명되지 않은 라우트 수를 드러낸다. 막지 않는 지표가 읽히지 않게 되는 것을 감수한다.
|
||||
배포는 막지 않고 서명되지 않은 라우트 수를 드러낸다. 막지 않는 지표가 읽히지 않게 되는 것을 감수한다 — 이 저장소에서 매번 우는 검사가 읽히지 않게 된 사례가 이미 있다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
|
||||
+11
-1
@@ -86,9 +86,19 @@ QUESTION → 열린 질문
|
||||
|
||||
두 안이 말하는 것은 같다. 「직접 해보니」와 「검증 기록」은 둘 다 그 글이 재현한 결과라고 말한다. 갈린 것은 이 사이트의 다른 글들이 쓰는 어조와 맞느냐다.
|
||||
|
||||
## 이름 표가 하나였기 때문에 두 번 바꿀 수 있었다
|
||||
|
||||
이름을 바꾸는 작업이 한 파일을 고치는 일이 됐다. 표가 화면마다 복사돼 있던 때였다면 두 번 바꾸는 동안 여섯 벌이 두 번씩 갈렸을 것이다.
|
||||
|
||||
같은 파일에 표가 하나 더 있다. 다섯 종류를 셋으로 접어 지식의 상태로 만드는 표다 — 확인한 것, 정리한 것, 아직 모르는 것.
|
||||
|
||||
> 미해결이 이 기록의 가장 정직한 신호인데 다섯 종류가 같은 회색 11px 로 나오면 그것이 가장 안 보인다.
|
||||
|
||||
이름을 바꿔도 이 표는 그대로였다. 표시 이름과 지식 상태가 다른 축이라 따로 두었기 때문이다.
|
||||
|
||||
## 계약의 kind 는 그대로 뒀다
|
||||
|
||||
바꾼 것은 화면에 보이는 이름이다. 계약의 `RecordKind` 는 다섯 값 그대로이고 주소도 그대로다.
|
||||
바꾼 것은 화면에 보이는 이름이다. 계약의 종류 값은 다섯 그대로이고 주소도 그대로다.
|
||||
|
||||
표시 이름과 계약 값을 갈라 두었기 때문에 두 번 바꾸면서 계약을 한 번도 건드리지 않았다. 계약을 바꿨다면 반입한 두 저장소가 함께 움직여야 했고, 이미 게시된 주소도 함께 흔들렸을 것이다.
|
||||
|
||||
|
||||
+25
-14
@@ -29,9 +29,13 @@ source:
|
||||
|
||||
## 사실
|
||||
|
||||
작업본 삭제 실패는 다섯 가지 이유가 전부 같은 한 문장으로 나온다 — `another record still links to this one; unlink it first`.
|
||||
작업본 삭제 실패는 다섯 참조 중 무엇이 막았든 같은 오류 코드로 나간다 — `DOCUMENT_IN_USE`.
|
||||
|
||||
실제로 막는 것은 다섯 참조 중 하나다.
|
||||
클라이언트에 나가는 문구는 코드마다 하나로 고정돼 있다. 그 코드의 문구는 「이 기록을 참조하는 곳이 있어 삭제할 수 없습니다」이다.
|
||||
|
||||
고정 문구를 두는 이유는 예외의 원문 메시지에 저장소 제약 이름이나 SQL 조각이 섞일 수 있어서다. 그 판단은 서버 쪽 클래스의 javadoc 에 적혀 있다.
|
||||
|
||||
참조 검사는 다섯 표를 하나의 존재 검사로 묶는다.
|
||||
|
||||
```sql
|
||||
SELECT 1 FROM document_relation WHERE target_document_id = :id
|
||||
@@ -41,47 +45,54 @@ UNION ALL SELECT 1 FROM topic_featured_document WHERE document_id = :id
|
||||
UNION ALL SELECT 1 FROM project_decision WHERE source_case_id = :id
|
||||
```
|
||||
|
||||
실제 사례는 「DTO 변환 과정에서 발생한 Highlight 컬렉션 N+1」이다. 관계를 다 지워도 삭제가 안 됐고, 남아 있던 것은 프로젝트 「Liner N + 1문제」로 가는 링크 한 행이었다.
|
||||
같은 어댑터의 질문 삭제는 참조가 둘뿐이라 같은 문제가 덜하다 — `project_question_link` 와 `home_focus_config.open_question_id` 다.
|
||||
|
||||
프로젝트 연결은 「관계」 편집기가 아니라 문서의 Project 필드다. 관계를 아무리 지워도 그 행은 남는다.
|
||||
|
||||
문구가 `another record` 라고 하니 관계를 찾아 지우게 되는데, 정작 막는 것은 record 가 아니라 프로젝트다. 문구가 잘못된 것을 가리키고 있다.
|
||||
실제 사례에서 관계를 다 지워도 삭제가 안 됐다. 남아 있던 것은 프로젝트 링크 한 행이었고, 그 링크는 「관계」 편집기가 아니라 문서의 Project 필드가 만든다.
|
||||
|
||||
사용자는 Project 필드를 「미지정」으로 바꾸고 저장한 뒤 삭제했다.
|
||||
|
||||
## 가정
|
||||
|
||||
문구가 `another record` 라고 말하므로 사용자가 관계를 먼저 찾는다고 보고 있다. 실제 사례가 하나이고, 다른 사용자가 같은 순서로 움직이는지는 확인하지 않았다.
|
||||
문구가 「another record」라고 하니 사용자가 관계를 먼저 찾는다고 보고 있다. 실제 사례가 하나이고, 다른 사용자가 같은 순서로 움직이는지는 확인하지 않았다.
|
||||
|
||||
다섯 참조를 종류별로 갈라도 성능이 문제가 되지 않는다고 보고 있다. 다섯 개의 존재 검사를 따로 돌리는 비용은 재지 않았다.
|
||||
다섯 참조를 종류별로 갈라도 응답 시간이 문제가 되지 않는다고 보고 있다. 하나의 존재 검사를 다섯 개로 나누는 비용은 재지 않았다.
|
||||
|
||||
질문 삭제처럼 참조가 둘뿐이면 이 문제가 덜하다고 보고 있다. 질문 삭제에서 사용자가 실제로 헤맨 기록은 없다.
|
||||
|
||||
## 미지수
|
||||
|
||||
무엇이 막는지 말하면서 내부 테이블 이름을 노출하지 않는 문구가 무엇인가.
|
||||
무엇이 막는지 말하면서 내부 표 이름을 노출하지 않는 문구가 무엇인가.
|
||||
|
||||
사용자가 고칠 수 있는 곳의 이름으로 옮기면 다섯 참조가 몇 가지로 줄어드는가. 문서의 Project 필드와 주제의 대표 기록은 서로 다른 화면이다.
|
||||
사용자가 고칠 수 있는 화면의 이름으로 옮기면 다섯 참조가 몇 가지로 줄어드는가. 문서의 Project 필드와 주제의 대표 기록은 서로 다른 화면이고, 관계 편집기는 또 다른 화면이다.
|
||||
|
||||
코드를 다섯으로 나누면 계약의 오류 코드 열거형도 함께 늘어난다. 그것을 반입하는 두 저장소가 그 값을 알아야 하는데, 그 비용이 얼마인가.
|
||||
|
||||
## 제약
|
||||
|
||||
클라이언트에 내보내는 메시지에 내부 테이블 이름이나 컬럼 이름을 넣지 않는다.
|
||||
클라이언트에 내보내는 메시지에 내부 표 이름이나 컬럼 이름을 넣지 않는다.
|
||||
|
||||
참조 검사는 삭제 경로에서 돈다. 이 경로의 응답 시간을 늘리지 않는다.
|
||||
|
||||
오류 코드는 계약이 열거한다. 코드를 늘리면 계약과 반입한 두 저장소가 함께 움직인다.
|
||||
|
||||
## 선택지
|
||||
|
||||
**참조 검사를 종류별로 갈라 어느 것이 걸렸는지 돌려준다**
|
||||
다섯 개의 존재 검사를 따로 돌리고 걸린 종류를 응답에 싣는다. 화면이 그 종류를 사용자가 고칠 수 있는 곳의 이름으로 옮긴다.
|
||||
다섯 개의 존재 검사를 따로 돌리고 걸린 종류를 응답에 싣는다. 화면이 그 종류를 사용자가 고칠 수 있는 화면의 이름으로 옮긴다. 코드를 늘리지 않고 응답의 부가 필드로 실으면 계약 변경이 작다.
|
||||
|
||||
**오류 코드를 참조 종류만큼 나눈다**
|
||||
문구가 코드마다 하나이므로 코드를 나누면 문구도 갈린다. 대신 계약의 열거형이 늘고 반입한 두 저장소가 함께 움직인다.
|
||||
|
||||
**막는 참조를 목록으로 돌려준다**
|
||||
어느 기록이 걸었는지까지 보인다. 관계는 이름을 보일 수 있지만 프로젝트 링크와 주제 대표 기록은 다른 화면이라 이름만으로는 어디를 고칠지 알기 어렵다.
|
||||
|
||||
**문구만 고쳐 프로젝트 연결을 함께 언급한다**
|
||||
가장 싸다. 다섯 중 어느 것인지는 여전히 말하지 못한다.
|
||||
가장 싸다. 다섯 중 어느 것인지는 여전히 말하지 못하고, 사용자가 확인할 화면이 셋으로 늘어난다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
1. 참조 검사를 종류별로 갈라 걸린 종류를 응답에 실어 보고, 삭제 경로의 응답 시간이 얼마나 달라지는지 잰다
|
||||
2. 다섯 종류를 사용자가 고칠 수 있는 화면 이름으로 옮겨 적고 몇 가지로 줄어드는지 센다
|
||||
3. 실제로 막힌 작업본 하나로 새 문구를 보여 주고 어디를 고쳐야 하는지 문구만으로 찾을 수 있는지 확인한다
|
||||
3. 실제로 막힌 작업본 하나로 새 문구를 보여 주고, 어디를 고쳐야 하는지 문구만으로 찾을 수 있는지 확인한다
|
||||
|
||||
닫는 조건 : 삭제가 막혔을 때 어디를 고쳐야 하는지 문구만 보고 알 수 있으면 닫는다. 종류별로 가르는 비용이 응답 시간에 드러나면 문구만 고치는 쪽으로 정하고 Decision 으로 넘긴다
|
||||
|
||||
+13
-1
@@ -57,7 +57,7 @@ Dockerfile 이 그 경고를 문자 그대로 적어 두고 있는데도 빠뜨
|
||||
|
||||
## 빌드 인자는 이미지에 굳는다
|
||||
|
||||
이 프론트는 API 주소를 빌드 인자로 받는다. 런타임 환경 변수가 아니므로 배포한 뒤에는 바꿀 수 없고, 잘못 넣으면 다시 빌드해서 다시 올려야 한다.
|
||||
이 프론트는 API 주소를 빌드 인자로 받는다. 번들러가 빌드할 때 그 값을 코드에 박아 넣으므로 런타임 환경 변수가 아니고, 배포한 뒤에는 바꿀 수 없다.
|
||||
|
||||
인자를 넘기지 않으면 기본값이 들어간다. 그 기본값은 존재하지 않는 주소다.
|
||||
|
||||
@@ -78,6 +78,18 @@ CI_RUNNER_IMAGE=node@sha256:… ← 반드시 @sha256 다이
|
||||
SOURCE_DATE_EPOCH
|
||||
```
|
||||
|
||||
## 인자마다 빠뜨렸을 때가 다르다
|
||||
|
||||
여섯 줄이 같은 무게는 아니다.
|
||||
|
||||
| 빠뜨리면 | 언제 드러나나 |
|
||||
|---|---|
|
||||
| API 주소 | 배포본이 첫 요청을 낼 때 |
|
||||
| 러너 이미지 다이제스트 | 빌드가 재현되지 않는다 — 나중에 같은 커밋이 다른 결과를 낸다 |
|
||||
| 빌드 식별자·커밋 SHA | 배포된 것이 어느 커밋인지 화면에서 못 읽는다 |
|
||||
|
||||
API 주소만 화면이 바로 깨지고 나머지는 조용하다. 그래서 이 목록에서 가장 눈에 띄는 것을 빠뜨렸다는 것이 오히려 이상한데, 인자가 여섯이고 그중 하나만 값의 모양이 URL 이라 붙여넣기에서 빠졌다.
|
||||
|
||||
## 되돌리고 다시 빌드했다
|
||||
|
||||
`kubectl rollout undo` 로 이전 리비전으로 되돌린 뒤 인자를 넣어 다시 빌드하고 다시 올렸다.
|
||||
|
||||
+3
-3
@@ -62,7 +62,7 @@ tech-log-frontend : 83409be
|
||||
|
||||
헬스 판정은 헬스 엔드포인트가 응답하는지를 본다. nginx 프로세스가 살아 있고 그 경로를 돌려주면 통과한다.
|
||||
|
||||
SPA 가 부팅에 필요한 설정 파일은 그 판정에 들어 있지 않다. 그래서 파드는 healthy 로 올라오고 사이트만 안 된다.
|
||||
SPA 가 부팅에 필요한 설정 파일은 그 판정에 들어 있지 않다. 그래서 파드는 healthy 로 올라오고 사이트만 안 된다. 롤아웃도 정상으로 끝나므로 배포 도구는 아무 신호도 주지 않는다.
|
||||
|
||||
## 빌드가 쓴 권한
|
||||
|
||||
@@ -70,7 +70,7 @@ SPA 가 부팅에 필요한 설정 파일은 그 판정에 들어 있지 않다.
|
||||
|
||||
증상이 404 가 아니라 403 이라는 것이 원인을 좁혔다. 404 면 파일이 없는 것이고 403 이면 파일은 있는데 읽지 못하는 것이므로, 이미지에 파일이 들어갔는지부터 확인할 필요가 없었다.
|
||||
|
||||
이미지가 권한을 정규화하도록 고쳤다.
|
||||
이미지가 권한을 정규화하도록 고쳤다. 빌드 단계에서 쓰는 권한을 바꾸는 대신 이미지가 마지막에 정리하게 한 것은, 빌드 도구가 그 권한을 왜 그렇게 쓰는지가 이 저장소 밖의 사정이기 때문이다.
|
||||
|
||||
## 브라우저가 묻는 주소
|
||||
|
||||
@@ -83,7 +83,7 @@ SPA 가 부팅에 필요한 설정 파일은 그 판정에 들어 있지 않다.
|
||||
| 설정 파일 | o | o | 권한이 0600 이라 읽지 못함 |
|
||||
| favicon | o | o | 아무도 그 이름을 요청하지 않음 |
|
||||
|
||||
둘 다 「파일이 없다」가 아니었고, 그래서 이미지를 열어 파일을 찾는 방식으로는 둘 다 정상으로 보였다.
|
||||
둘 다 「파일이 없다」가 아니었다. 그래서 이미지를 열어 파일을 찾는 방식으로는 둘 다 정상으로 보인다 — 무엇이 실제로 요청되고 어떤 응답이 오는지를 봐야 갈린다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
|
||||
+19
-1
@@ -66,6 +66,8 @@ DB : 실제 PostgreSQL (Testcontainers)
|
||||
|
||||
> 그 쿼리의 여섯 컬럼 중 다섯은 마이그레이션과 대조했다. 이 하나만 가정했고, 그것이 틀렸다.
|
||||
|
||||
같은 어댑터의 다른 참조 검사들은 문서 식별자 컬럼을 가진 표를 조회하므로 그 이름이 맞는다. 한 함수 안에서 표마다 컬럼 이름이 다른데, 다섯 개가 맞으니 여섯 번째도 맞을 것으로 읽었다.
|
||||
|
||||
## 그 SQL 은 한 번도 실행되지 않았다
|
||||
|
||||
컬럼 이름보다 더 드러난 것은 검사 구조였다.
|
||||
@@ -79,6 +81,22 @@ DB : 실제 PostgreSQL (Testcontainers)
|
||||
|
||||
표준 검사가 컨테이너를 띄우지 않으므로 어댑터의 SQL 은 한 줄도 실행되지 않은 채 빌드가 통과한다. 컬럼 이름이 맞는지 묻는 검사가 어디에도 없었다.
|
||||
|
||||
## 문자열로 조립한 SQL 은 컴파일러가 보지 않는다
|
||||
|
||||
이 어댑터는 SQL 을 문자열로 이어 붙여 만든다.
|
||||
|
||||
```java
|
||||
"SELECT EXISTS ("
|
||||
+ " SELECT 1 FROM document_relation WHERE target_document_id = :id"
|
||||
+ " UNION ALL SELECT 1 FROM question_document_link WHERE document_id = :id"
|
||||
+ " UNION ALL SELECT 1 FROM project_document_link WHERE document_id = :id"
|
||||
+ " UNION ALL SELECT 1 FROM topic_featured_document WHERE document_id = :id"
|
||||
+ " UNION ALL SELECT 1 FROM project_decision WHERE source_case_id = :id"
|
||||
+ ")"
|
||||
```
|
||||
|
||||
컴파일러가 확인하는 것은 이 식이 문자열이라는 것까지다. 표 이름도 컬럼 이름도 실행해야 검증된다.
|
||||
|
||||
## 전용 태스크로 여덟 시나리오를 돌린다
|
||||
|
||||
삭제 경로 전용 통합 테스트 태스크를 만들고, 실패했던 그 쿼리를 포함해 여덟 시나리오를 실제 PostgreSQL 에서 돌린다.
|
||||
@@ -89,6 +107,6 @@ DB : 실제 PostgreSQL (Testcontainers)
|
||||
|
||||
이 태스크가 덮는 것은 삭제 경로다. 표준 검사는 여전히 컨테이너를 띄우지 않고, 새 어댑터 SQL 이 이 태스크에 등록되는지 보는 검사도 없다.
|
||||
|
||||
같은 참조 검사가 지금도 다섯 테이블을 하나로 묶어 확인하므로, 무엇이 막았는지를 응답이 말하지 못한다.
|
||||
같은 참조 검사가 지금도 다섯 표를 하나로 묶어 확인하므로, 무엇이 막았는지를 응답이 말하지 못한다.
|
||||
|
||||
<!-- body:end -->
|
||||
|
||||
@@ -3029,5 +3029,5 @@
|
||||
"unlisted": 0,
|
||||
"candidates": 91
|
||||
},
|
||||
"ssotSha256": "6fbb5ffd943df5a6ce53624997f9ebc5b3c80de9c0adf17f62af951f8e55488d"
|
||||
"ssotSha256": "75e2c6368a5f6866ad72956619d020c89d4442428a79093fec29b1dfc647490c"
|
||||
}
|
||||
|
||||
+8
@@ -64,6 +64,8 @@ tech-log-frontend : fd73bc8 · fe6b56a
|
||||
|
||||
> 화면 테스트는 `test:unit` 이 아니라 `test:tech-log` 가 돌린다. 그것을 돌리지 않아 위 두 결함과, 의도한 변경에 고정돼 있던 단언들이 **23건 빨간 채로 여러 커밋을 지나갔다.**
|
||||
|
||||
두 명령이 보는 디렉터리가 다르다. 단위 테스트 명령은 단위 테스트 디렉터리를, 화면 테스트 명령은 기능별 테스트 디렉터리를 돈다. 넷을 돌리고 「전부 통과」라고 읽으면 다섯 번째가 빠진다.
|
||||
|
||||
23건이 한 종류가 아니었다는 것이 문제를 키웠다. 일부는 실제 결함을 잡은 것이고 일부는 의도한 변경에 단언이 고정돼 있던 것이다. 둘을 구분하려면 그 명령을 돌려 하나씩 봐야 하는데, 돌리지 않으니 둘 다 그대로 남았다.
|
||||
|
||||
## 게이트 기준값을 빠뜨렸다
|
||||
@@ -85,6 +87,12 @@ tech-log-frontend : fd73bc8 · fe6b56a
|
||||
|
||||
> **가드는 CI 에 묶여야 의미가 있습니다.** 사람이 기억해서 돌리는 가드는 절반만 존재합니다.
|
||||
|
||||
## 명령이 많으면 「다 돌렸다」가 흐려진다
|
||||
|
||||
이 저장소의 프론트엔드는 검사 명령이 수십 개다. 타입 검사만 여섯 개의 tsconfig 를 돌고, 테스트도 단위·컴포넌트·통합·기능별로 나뉜다.
|
||||
|
||||
명령을 나눈 이유는 각각 다른 대상을 보기 때문이고 그 자체는 맞다. 대신 「배포 전에 무엇을 돌리는가」를 목록으로 고정하지 않으면 그중 일부만 돌고도 전부 돌린 것으로 읽힌다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
다섯 명령을 CI 에 묶는 작업은 하지 않았다. 지금 남은 것은 메모리와 배포 전 검증 목록이고, 그 목록을 읽는 것도 사람이다.
|
||||
|
||||
Reference in New Issue
Block a user