refactor: 문서 개선 중
This commit is contained in:
+1
-1
@@ -28,7 +28,7 @@ source:
|
||||
|
||||
## 문제
|
||||
|
||||
주제 화면의 네 줄(SPA·Mediator·BFF·Forward-Auth)은 링크로 그려져 있었다. 눌러도 아무 일이 없었다.
|
||||
주제 화면의 네 줄(SPA(Single-Page Application)·Mediator·BFF(Backend for Frontend)·Forward-Auth)은 링크로 그려져 있었다. 눌러도 아무 일이 없었다.
|
||||
|
||||
처음에 /topics/{주제}/{축} 이라 적어 두었는데 그런 화면이 없었다.
|
||||
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ topic (주제)
|
||||
|
||||
## 기록은 여러 축에 걸린다
|
||||
|
||||
한 기록이 여러 축에 걸릴 수 있다. PKCE 는 SPA 와 BFF 양쪽에 관계된다.
|
||||
한 기록이 여러 축에 걸릴 수 있다. PKCE(Proof Key for Code Exchange)는 SPA(Single-Page Application)와 BFF(Backend for Frontend) 양쪽에 걸린다.
|
||||
|
||||
아무 축에도 걸리지 않은 기록은 그 주제의 공통 기록으로 읽는다. 「공통」이라는 축을 따로 만들지 않는 이유는, 만들면 그 축이 비교 화면에 한 줄로 서서 다른 축들과 견주는 것처럼 보이기 때문이다.
|
||||
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ source:
|
||||
|
||||
## 판단 이유
|
||||
|
||||
주제를 넷으로 쪼개면 네 구조가 함께 쓰는 기록을 어디에 둘지 애매해진다. PKCE·CSRF·Authorization Code 가 그런 기록이다. 어느 한 주제에 넣으면 나머지 셋에서 그 기록에 닿을 수 없고, 넷에 복사하면 같은 글이 넷이 된다.
|
||||
주제를 넷으로 쪼개면 네 구조가 함께 쓰는 기록을 어디에 둘지 애매해진다. PKCE(Proof Key for Code Exchange)와 CSRF(Cross-Site Request Forgery) 방어, Authorization Code 흐름이 그런 기록이다. 어느 한 주제에 넣으면 나머지 셋에서 그 기록에 닿을 수 없고, 넷에 복사하면 같은 글이 넷이 된다.
|
||||
|
||||
비교도 어려워진다. 네 주제가 나란히 서면 그것이 같은 질문의 네 답이라는 것을 화면이 말하지 못하고, 독자는 목록에서 넷을 각각 열어 봐야 한다.
|
||||
|
||||
|
||||
+2
@@ -106,6 +106,8 @@ tech-log-frontend : 344dadb · 805d400 · 8c5dbe1
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
이 결함이 났던 당시의 before/after 브라우저 캡처는 저장돼 있지 않다. 현재 화면을 당시 상태처럼 다시 꾸며 과거 증거로 쓰지 않는다.
|
||||
|
||||
CSS module 을 쓰는 화면은 전역 규칙이 닿지 않아 따로 고쳤다. 「지금 집중하는 것」 탭과 주제 탭의 표시 방식이 아직 다르다 — 한쪽은 파란 밑줄이고 한쪽은 알약이다. 주제 탭 밑줄을 그쪽에서 베껴 왔다가 뺐기 때문이다.
|
||||
|
||||
<!-- body:end -->
|
||||
|
||||
+2
@@ -88,6 +88,8 @@ tech-log-frontend : 68538f2
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
이 결함이 났던 당시의 before/after 브라우저 캡처는 저장돼 있지 않다. 현재 화면을 당시 상태처럼 다시 꾸며 과거 증거로 쓰지 않는다.
|
||||
|
||||
이 검사가 보는 것은 CSS 선언이다. 실제로 그려진 크기는 아니다 — 다른 규칙이 덮으면 선언이 같아도 결과가 갈릴 수 있다.
|
||||
|
||||
<!-- body:end -->
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ tech-log-frontend : 15e6ea8 이후
|
||||
|
||||
축 CRUD 를 더한 커밋에서 가드를 둘 넣었다. 공개 계약은 전수 대조한다 — 계약이 선언한 연산이 기여 목록에 전부 있는지 본다.
|
||||
|
||||
관리 계약은 86 operation 이라 전수 대조가 무겁다. 대신 「한 종류만 빠진 항목」을 본다. 깨진 것이 늘 그 모양이었기 때문이다.
|
||||
관리 계약에는 86 operation 이 있다. 가드 범위를 좁힌 근거는 그 숫자 자체가 아니라 당시 반복해서 깨진 형태가 「한 종류만 빠진 항목」이었다는 점이다. 그래서 이 가드는 그 패턴을 보며, 연산 전체 누락까지 전수 대조한다고 말하지 않는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
|
||||
+6
-16
@@ -54,28 +54,18 @@ source:
|
||||
|
||||
모델 생성은 스키마와 속성만 본다. 구현이 없어도 모델은 멀쩡히 만들어진다.
|
||||
|
||||
### 6. 전수 대조가 무거우면 깨지는 모양으로 좁힌다
|
||||
### 6. 관찰된 실패 모양으로 검사 범위를 좁힐 때는 한계를 적는다
|
||||
|
||||
관리 계약은 86 operation 이라 전수 대조가 무겁다. 이 저장소는 대신 「한 종류만 빠진 항목」을 보게 했다 — 깨진 것이 늘 그 모양이었기 때문이다. 좁힌 기준은 무엇을 보지 않는지도 함께 적는다.
|
||||
관리 계약에는 86 operation 이 있다. 검사 범위를 좁힌 이유는 그 숫자 자체가 아니라 당시 반복해서 깨진 형태가 「한 종류만 빠진 항목」이었기 때문이다. 그래서 관리 계약 쪽 가드는 그 패턴을 보며, 전체 계약과 구현의 동등성을 증명한다고 말하지 않는다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
- 계약이 한 저장소에 있고 두 저장소가 그것을 반입해 각자 구현하는 구조. 연산을 더하거나 지우는 변경에서 이 대조를 돌린다.
|
||||
이 대조가 필요한 때는 계약이 한 저장소에 있고 두 저장소가 그것을 반입해 각자 구현하는 구조에서 연산을 더하거나 지울 때다. 화면이 「데이터가 없습니다」를 그리는데 저장소에는 값이 있을 때도 먼저 본다. 구현이 없어서 404가 난 경우와 정말 0건인 경우가 화면에서는 같은 빈 상태로 보이기 때문이다.
|
||||
|
||||
- 화면이 「데이터가 없습니다」를 그리는데 저장소에는 값이 있을 때 이 대조를 먼저 본다. 구현이 없어서 404 인 것과 정말 0건인 것이 화면에서 같아 보인다.
|
||||
실제 검증에서는 매핑 하나를 떼었을 때 대조 검사가 그 연산 하나를 정확히 짚는 것을 확인한 뒤 커밋했다. 두 목록 조회에 컨트롤러가 없던 때에는 홈 편집기가 「이 프로젝트에 열린 질문이 없습니다」를 그렸지만 실제로는 넷이 있었고 공개 사이트에도 나오고 있었다. 화면 쪽에서는 기여 목록에 등록하지 않은 연산 넷을 만났고, 둘은 옆 분기로 떨어져 다른 기록을 다뤘으며 둘은 빈 목록이 됐다.
|
||||
|
||||
## 예외
|
||||
|
||||
- 계약과 구현이 같은 저장소에 있고 같은 빌드를 지나면 컴파일러가 이 대조를 대신한다.
|
||||
계약과 구현이 같은 저장소에 있어도 컴파일러가 대신할 수 있는 범위는 실제로 같은 타입이나 생성 산출물에 연결된 곳뿐이다. 문자열 라우트나 별도 컨트롤러 등록처럼 타입 시스템 밖에 있는 항목은 별도 대조가 필요하다. 봉투 규약을 따르지 않는 연산을 경로 대조에서 빼야 한다면, 제외 이유는 명시 목록에 남긴다.
|
||||
|
||||
- 연산이 봉투 규약을 따르지 않으면 경로 대조에서 뺀다. 다만 뺀 이유를 목록에 적는다.
|
||||
|
||||
## 예시
|
||||
|
||||
- 매핑 하나를 떼어 보고 대조 검사가 그 연산 하나를 정확히 짚는 것을 확인한 뒤 커밋했다.
|
||||
|
||||
- 두 목록 조회에 컨트롤러가 없어 홈 편집기가 「이 프로젝트에 열린 질문이 없습니다」를 그렸다. 실제로는 넷이 있었고 공개 사이트에도 나오고 있었다.
|
||||
|
||||
- 옛 연산 51개를 명시하지 않았다면 대조 결과가 51건의 실패로 나와 아무도 읽지 않았을 것이다.
|
||||
|
||||
- 기여 목록에 등록하지 않은 연산 넷을 만났다. 둘은 옆 분기로 떨어져 다른 기록을 다뤘고 둘은 빈 목록이 됐다.
|
||||
옛 연산 51개는 의도적으로 구현하지 않는 목록으로 분리해 대조 결과의 잡음을 줄였다. 이 면제 목록은 「구현이 빠진 것」과 「구현하지 않기로 한 것」을 구분하기 위한 것이며, 목록 밖의 누락까지 면제하지 않는다.
|
||||
+5
-3
@@ -74,11 +74,13 @@ tech-log-frontend : 6e784ed · fd73bc8 · 3bb724b
|
||||
|
||||
거절만 잡는 처리로는 부족하다. 던지는 경로도 함께 잡아야 한 칸의 실패가 화면 전체로 번지지 않는다.
|
||||
|
||||
## 왜 동기적으로 던질 수 있나
|
||||
## 언제 동기적으로 던질 수 있나
|
||||
|
||||
게이트웨이 호출이 비동기 함수여도 그 안의 첫 줄이 동기적으로 실행된다. 인자를 검증하거나 연산을 고르는 코드가 거기 있고, 등록되지 않은 연산을 고르면 거기서 바로 던진다.
|
||||
`async function` 본문에서 던진 예외는 호출자에게 rejected Promise 로 전달된다. 이 경우에는 `Promise.all` 의 rejection 경로로 들어간다.
|
||||
|
||||
그래서 「비동기 함수를 불렀으니 거절로 온다」는 전제가 성립하지 않는다.
|
||||
별도로 봐야 하는 것은 Promise 를 반환하는 API처럼 보이지만 실제 구현이 일반 함수이고, Promise 를 만들기 전에 인자 검증이나 연산 선택 같은 동기 코드가 실행되는 경우다. 그 코드가 throw 하면 함수 호출 자체가 동기적으로 실패해 배열이 완성되지 않고 `Promise.all` 에 도달하지 못한다.
|
||||
|
||||
따라서 이 사건에서 확인할 기준은 「비동기 작업인가」가 아니라 「호출이 Promise 를 반환하기 전에 동기 throw 할 수 있는가」다.
|
||||
|
||||
## 탭에도 같은 판단을
|
||||
|
||||
|
||||
+7
@@ -9,6 +9,9 @@ status: 게시 전
|
||||
lastVerifiedOn: 2026-09-04
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/guards/kind-tables-now.txt
|
||||
assets:
|
||||
- key: record-kind-fanout
|
||||
file: ../../../final/assets/diagrams/record-kind-fanout/record-kind-fanout.svg
|
||||
sourceRevision: tech-log@2026-09-02
|
||||
source:
|
||||
- final/document.md#§3.1
|
||||
@@ -90,6 +93,10 @@ CONCEPT 을 더해도 이 코드는 컴파일된다. 마지막 가지가 나머
|
||||
|
||||
## 열세 곳
|
||||
|
||||
열세 위치를 다시 카드로 늘어놓지 않고, `CONCEPT` 하나가 계약·백엔드·프론트엔드의 손 목록으로 퍼진 구조만 묶어서 본다. 정확한 열세 위치는 바로 아래 표가 맡는다.
|
||||
|
||||

|
||||
|
||||
| # | 어디 | 증상 | 커밋 |
|
||||
|---|---|---|---|
|
||||
| 1 | 게이트웨이의 문서 삭제 분기 | 개념을 지우면 "질문을 찾을 수 없습니다" | `dec86bd` |
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ source:
|
||||
|
||||
# nginx 가 모르는 라우트는 새로고침에서 404 다
|
||||
|
||||
/studio/releases 가 평문 404 를 돌려줬다. 라우트는 있고 청크도 빌드됐고 SPA 내부 이동으로는 화면에 닿는데, 하드 로드와 새로고침은 거기까지 가지 못한다. nginx 설정이 손으로 유지하는 배열에서 나오고 있었다.
|
||||
/studio/releases 가 평문 404 를 돌려줬다. 라우트는 있고 청크도 빌드됐고 SPA(Single-Page Application) 내부 이동으로는 화면에 닿는데, 하드 로드와 새로고침은 거기까지 가지 못한다. nginx 설정이 손으로 유지하는 배열에서 나오고 있었다.
|
||||
|
||||
## 관계
|
||||
|
||||
|
||||
+8
-1
@@ -7,6 +7,9 @@ topicName: 라우트 하나가 울리는 손 목록
|
||||
project: TechLog
|
||||
status: 게시 전
|
||||
lastVerifiedOn: 2026-09-04
|
||||
assets:
|
||||
- key: route-fanout
|
||||
file: ../../../final/assets/diagrams/route-fanout/route-fanout.svg
|
||||
sourceRevision: tech-log@2026-09-02
|
||||
source:
|
||||
- final/document.md#§8.1
|
||||
@@ -45,7 +48,7 @@ source:
|
||||
## 검증 환경
|
||||
|
||||
tech-log-frontend : 048c1b2 · 197db74 · fe6b56a
|
||||
CI : FE-GATE-009 — 라우트마다 수동 접근성 증거 1개
|
||||
CI : 프론트엔드 게이트 `FE-GATE-009` — 라우트마다 수동 접근성 증거 1개
|
||||
확인 방식 : 라우트를 더한 커밋 넷에서 기준값이 어떻게 움직였는지 대조
|
||||
|
||||
## 재현 조건
|
||||
@@ -60,6 +63,10 @@ CI : FE-GATE-009 — 라우트마다 수동 접근성 증거 1개
|
||||
|
||||
## 여덟 곳
|
||||
|
||||
라우트 계약 하나에서 런타임·빌드·배포 검사가 갈라지고, 빠뜨린 곳에 따라 처음 드러나는 시점도 달라진다.
|
||||
|
||||

|
||||
|
||||
개념 라우트를 더한 커밋이 그 목록을 남겼다.
|
||||
|
||||
```text
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ source:
|
||||
|
||||
라우트를 더할 때마다 서빙 패턴이 함께 움직인다. 이 비용은 라우트 계약에서 유도해 없앴다 — 손으로 배열을 고치지 않는다.
|
||||
|
||||
등록되지 않은 주소는 SPA 에 닿지 못한다. 라우트를 더하고 프론트를 배포하기 전까지 그 경로는 엣지에서 404 이고, 그래서 새 라우트는 프론트를 먼저 배포한다.
|
||||
등록되지 않은 주소는 SPA(Single-Page Application)에 닿지 못한다. 라우트를 더하고 프론트를 배포하기 전까지 그 경로는 엣지에서 404 이고, 그래서 새 라우트는 프론트를 먼저 배포한다.
|
||||
|
||||
배포 뒤 감사에서 200 을 받은 35개 주소는 실제로 화면이 그려지는 주소다. catch-all 을 번역했다면 그 수는 아무 주소나 세어도 나왔을 것이다.
|
||||
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ CI 게이트가 설치된 라우트마다 수동 접근성 증거 파일을 하
|
||||
|
||||
## 사실
|
||||
|
||||
FE-GATE-009 는 설치된 라우트마다 증거 파일 하나를 요구하고, 그 집합이 정확히 일치하지 않으면 거절한다.
|
||||
`FE-GATE-009` 프론트엔드 게이트는 설치된 라우트마다 증거 파일 하나를 요구하고, 그 집합이 정확히 일치하지 않으면 거절한다.
|
||||
|
||||
정확한 일치를 요구하는 이유는 빠뜨림이 통과가 되지 않게 하려는 것이다. 파일이 더 많아도 더 적어도 거절한다.
|
||||
|
||||
@@ -56,7 +56,7 @@ review:a11y-manual 스크립트는 그래서 실패하는 것이 지금은 정
|
||||
|
||||
## 제약
|
||||
|
||||
FE-GATE-009 는 라우트 집합과 증거 집합이 정확히 일치하기를 요구한다. 이 규칙은 바꾸지 않는다 — 빠뜨림이 통과가 되면 게이트가 아니다.
|
||||
프론트엔드 게이트 `FE-GATE-009`는 라우트 집합과 증거 집합이 정확히 일치하기를 요구한다. 이 규칙은 바꾸지 않는다 — 빠뜨림이 통과가 되면 게이트가 아니다.
|
||||
|
||||
수동 접근성 증거는 사람이 만든다. 자동 검사로 대신하지 않는다.
|
||||
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ source:
|
||||
|
||||
### 2. 빌드가 아는 목록을 서빙 계약의 근거로 쓰지 않는다
|
||||
|
||||
번들된 픽스처에 우연히 들어 있던 경로를 열거하면 그 목록이 빌드 시점에 얼어붙는다. location = 은 정확히 일치하는 경로만 잡으므로, 빌드 이후에 게시된 기록은 SPA 에 묻기도 전에 엣지에서 404 가 된다.
|
||||
번들된 픽스처에 우연히 들어 있던 경로를 열거하면 그 목록이 빌드 시점에 얼어붙는다. location = 은 정확히 일치하는 경로만 잡으므로, 빌드 이후에 게시된 기록은 SPA(Single-Page Application)에 묻기도 전에 엣지에서 404 가 된다.
|
||||
|
||||
### 3. 유도할 수 없는 목록에는 대조 검사를 둔다
|
||||
|
||||
|
||||
+4
-12
@@ -35,25 +35,17 @@ source:
|
||||
|
||||
고정 문구를 두는 이유는 예외의 원문 메시지에 저장소 제약 이름이나 SQL 조각이 섞일 수 있어서다. 그 판단은 서버 쪽 클래스의 javadoc 에 적혀 있다.
|
||||
|
||||
참조 검사는 다섯 표를 하나의 존재 검사로 묶는다.
|
||||
|
||||
sql
|
||||
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은 이 기록의 근거인 `final/document.md` §16.1에 정상 SQL 코드 블록으로 남겨 두었다.
|
||||
|
||||
같은 어댑터의 질문 삭제는 참조가 둘뿐이라 같은 문제가 덜하다 — project_question_link 와 home_focus_config.open_question_id 다.
|
||||
|
||||
실제 사례에서 관계를 다 지워도 삭제가 안 됐다. 남아 있던 것은 프로젝트 링크 한 행이었고, 그 링크는 「관계」 편집기가 아니라 문서의 Project 필드가 만든다.
|
||||
실제 사례에서 Studio의 「관계」 편집기로 만든 연결을 다 지워도 삭제가 안 됐다. 남아 있던 것은 `project_document_link` 한 행이었고, 그 링크는 「관계」 편집기가 아니라 문서의 Project 필드가 만든다.
|
||||
|
||||
사용자는 Project 필드를 「미지정」으로 바꾸고 저장한 뒤 삭제했다.
|
||||
|
||||
## 가정
|
||||
|
||||
문구가 「another record」라고 하니 사용자가 관계를 먼저 찾는다고 보고 있다. 실제 사례가 하나이고, 다른 사용자가 같은 순서로 움직이는지는 확인하지 않았다.
|
||||
문구가 「another record」라고 하니 사용자가 Studio의 「관계」 편집기를 먼저 확인한다고 보고 있다. 실제 사례가 하나이고, 다른 사용자가 같은 순서로 움직이는지는 확인하지 않았다.
|
||||
|
||||
다섯 참조를 종류별로 갈라도 응답 시간이 문제가 되지 않는다고 보고 있다. 하나의 존재 검사를 다섯 개로 나누는 비용은 재지 않았다.
|
||||
|
||||
@@ -84,7 +76,7 @@ UNION ALL SELECT 1 FROM project_decision WHERE source_case_id = :id
|
||||
문구가 코드마다 하나이므로 코드를 나누면 문구도 갈린다. 대신 계약의 열거형이 늘고 반입한 두 저장소가 함께 움직인다.
|
||||
|
||||
**막는 참조를 목록으로 돌려준다**
|
||||
어느 기록이 걸었는지까지 보인다. 관계는 이름을 보일 수 있지만 프로젝트 링크와 주제 대표 기록은 다른 화면이라 이름만으로는 어디를 고칠지 알기 어렵다.
|
||||
어느 기록이 걸었는지까지 보인다. 문서 relation은 연결된 기록 이름을 보일 수 있지만 프로젝트 링크와 주제 대표 기록은 다른 화면이라 이름만으로는 어디를 고칠지 알기 어렵다.
|
||||
|
||||
**문구만 고쳐 프로젝트 연결을 함께 언급한다**
|
||||
가장 싸다. 다섯 중 어느 것인지는 여전히 말하지 못하고, 사용자가 확인할 화면이 셋으로 늘어난다.
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ source:
|
||||
- **배포 전에 사람이 돌려야 하는 것과 그 함정**
|
||||
이 사건 뒤에 목록으로 굳혔다.
|
||||
- **컨테이너는 healthy 였고 SPA 가 부팅에 필요한 파일 하나만 403 이었다**
|
||||
같은 배포에서 드러난 다른 사건이다.
|
||||
같은 배포에서 드러난 다른 사건이다. 여기서 SPA는 Single-Page Application을 뜻한다.
|
||||
|
||||
## 문제
|
||||
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ source:
|
||||
|
||||
# 컨테이너는 healthy 였고 SPA 가 부팅에 필요한 파일 하나만 403 이었다
|
||||
|
||||
컨테이너는 healthy 로 올라왔는데 SPA 가 부팅되지 않았다. nginx 가 설정 파일 하나를 읽지 못해 그 파일만 403 을 돌려줬다. 빌드가 그 파일을 0600 으로 쓰고 있었다.
|
||||
컨테이너는 healthy 로 올라왔는데 SPA(Single-Page Application)가 부팅되지 않았다. nginx 가 설정 파일 하나를 읽지 못해 그 파일만 403 을 돌려줬다. 빌드가 그 파일을 0600 으로 쓰고 있었다.
|
||||
|
||||
## 관계
|
||||
|
||||
@@ -68,7 +68,7 @@ SPA 가 부팅에 필요한 설정 파일은 그 판정에 들어 있지 않다.
|
||||
|
||||
빌드가 그 설정 파일을 0600 으로 쓴다. 파일을 만든 사용자만 읽을 수 있고, nginx 를 돌리는 사용자는 다른 사용자다.
|
||||
|
||||
증상이 404 가 아니라 403 이라는 것이 원인을 좁혔다. 404 면 파일이 없는 것이고 403 이면 파일은 있는데 읽지 못하는 것이므로, 이미지에 파일이 들어갔는지부터 확인할 필요가 없었다.
|
||||
증상이 404 가 아니라 403 이라는 것은 이 nginx 구성에서 파일 접근 권한을 먼저 의심할 단서였다. 다만 HTTP 403 자체가 「파일은 존재하지만 읽지 못한다」를 보장하지는 않는다. nginx 의 deny 규칙이나 앞단 인증·인가에서도 403 이 날 수 있다. 이 사건은 이미지 안의 파일 mode 가 0600 인 것을 확인하면서 권한 문제로 확정했다.
|
||||
|
||||
이미지가 권한을 정규화하도록 고쳤다. 빌드 단계에서 쓰는 권한을 바꾸는 대신 이미지가 마지막에 정리하게 한 것은, 빌드 도구가 그 권한을 왜 그렇게 쓰는지가 이 저장소 밖의 사정이기 때문이다.
|
||||
|
||||
|
||||
+9
-4
@@ -22,7 +22,7 @@ source:
|
||||
- **배포 인자를 빠뜨려 배포본이 존재하지 않는 주소를 불렀다**
|
||||
이 경로에서 빌드 인자가 어떻게 새는지가 그 기록에 있다.
|
||||
- **컨테이너는 healthy 였고 SPA 가 부팅에 필요한 파일 하나만 403 이었다**
|
||||
이미지 안의 권한이 배포에서 드러난 사건이다.
|
||||
이미지 안의 권한이 배포에서 드러난 사건이다. 여기서 SPA는 Single-Page Application을 뜻한다.
|
||||
- **배포 전에 사람이 돌려야 하는 것과 그 함정**
|
||||
이 경로에서 사람이 기억해야 하는 것들이 그 기준에 있다.
|
||||
|
||||
@@ -32,12 +32,17 @@ source:
|
||||
|
||||
## 레지스트리를 쓰지 않는다
|
||||
|
||||
이 블록은 복사해 실행하는 runbook이 아니라 배포 단계의 순서만 보여 주는 reference schematic이다.
|
||||
|
||||
```text
|
||||
로컬 docker build → docker save | gzip → scp dh-server:/tmp/deploy.tar.gz
|
||||
→ kube-system 의 containerd import Job → kubectl set image
|
||||
로컬 이미지 빌드
|
||||
→ 이미지 tar 압축
|
||||
→ 서버로 전송
|
||||
→ 일회성 containerd import Job
|
||||
→ 배포 이미지 교체
|
||||
```
|
||||
|
||||
공개 Hub 는 소스가 들어간 이미지라 쓸 수 없다. k3s 의 containerd 소켓은 root 전용이라 사용자 셸에서 닿지 않는다. 그래서 클러스터 안에 일회성 Job 을 띄워 tar 를 import 한다 — Job 은 클러스터 권한으로 도므로 그 소켓에 닿는다.
|
||||
공개 Hub 는 소스가 들어간 이미지라 쓸 수 없다. k3s 의 containerd 소켓은 root 전용이라 사용자 셸에서 닿지 않는다. 그래서 클러스터 안의 일회성 Job 으로 tar 를 import 하는 경로를 쓴다. 다만 Job 이 `kube-system` 에 있거나 클러스터 RBAC 권한을 가진다는 이유만으로 host 소켓에 접근할 수 있는 것은 아니다. 이 경로에는 host 의 containerd 소켓을 명시적으로 mount 하고 그 소켓을 열 수 있는 권한으로 실행한다는 전제가 필요하다.
|
||||
|
||||
배포 단위는 `hyeonworks.com` 하나이고 서브도메인을 쓰지 않는다. 공개는 `/`, API 는 `/api` 다.
|
||||
|
||||
|
||||
+7
@@ -7,6 +7,9 @@ topicName: 테스트가 지나지 않는 이음매
|
||||
project: TechLog
|
||||
status: 게시 전
|
||||
lastVerifiedOn: 2026-09-04
|
||||
assets:
|
||||
- key: composition-root-seam
|
||||
file: ../../../final/assets/diagrams/composition-root-seam/composition-root-seam.svg
|
||||
sourceRevision: tech-log@2026-09-02
|
||||
source:
|
||||
- final/document.md#§7.4
|
||||
@@ -80,6 +83,10 @@ tech-log-frontend : 03986da · 7600711
|
||||
|
||||
## 스텁이 이음매를 덮지 않는다
|
||||
|
||||
화면 테스트는 게이트웨이에서, 게이트웨이 테스트는 실행기에서 스텁으로 끊겼다. 실제 런타임에서만 이어지는 합성 루트의 credential 결정은 두 테스트 경로 사이에 비어 있었다.
|
||||
|
||||

|
||||
|
||||
> 이 결함은 공개 소스가 HTTP 가 된 뒤에야 나타날 수 있었다. 이번 주까지 그 경로는 브라우저에서 한 번도 돌지 않았다. **스위트가 잡지 못한 이유는 게이트웨이와 화면을 검사할 뿐 합성 루트의 credential 결정은 검사하지 않기 때문이다 — 그 이음매에는 테스트가 없고, 이것이 그 대가다.**
|
||||
|
||||
게이트웨이 테스트는 실행기를 스텁으로 바꾸고 화면 테스트는 게이트웨이를 스텁으로 바꾼다. 둘 다 자기 층은 검사하지만 그 사이에서 credential 을 정하는 코드는 어느 쪽에도 들어가지 않는다.
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
],
|
||||
"sourceRevision": "tech-log@2026-09-02",
|
||||
"generatedAt": "2026-09-07",
|
||||
"generatedAt": "2026-09-18",
|
||||
"candidateScope": {
|
||||
"document": "final/document.md",
|
||||
"sections": [
|
||||
@@ -125,8 +125,12 @@
|
||||
"file": "hand-listed-kinds/case/case-one-new-kind-fell-through-thirteen-places.md",
|
||||
"status": "게시 전",
|
||||
"studioId": "",
|
||||
"assets": [],
|
||||
"assetFiles": [],
|
||||
"assets": [
|
||||
"record-kind-fanout"
|
||||
],
|
||||
"assetFiles": [
|
||||
"record-kind-fanout"
|
||||
],
|
||||
"evidenceFiles": [
|
||||
"../../../final/evidence/raw/guards/kind-tables-now.txt"
|
||||
]
|
||||
@@ -388,8 +392,12 @@
|
||||
"file": "values-lost-between-boundaries/case/case-a-summary-vanished-at-three-boundaries.md",
|
||||
"status": "게시 전",
|
||||
"studioId": "",
|
||||
"assets": [],
|
||||
"assetFiles": [],
|
||||
"assets": [
|
||||
"summary-drop-path"
|
||||
],
|
||||
"assetFiles": [
|
||||
"summary-drop-path"
|
||||
],
|
||||
"evidenceFiles": []
|
||||
},
|
||||
{
|
||||
@@ -703,8 +711,12 @@
|
||||
"file": "seams-no-test-crosses/case/case-the-composition-root-had-no-test.md",
|
||||
"status": "게시 전",
|
||||
"studioId": "",
|
||||
"assets": [],
|
||||
"assetFiles": [],
|
||||
"assets": [
|
||||
"composition-root-seam"
|
||||
],
|
||||
"assetFiles": [
|
||||
"composition-root-seam"
|
||||
],
|
||||
"evidenceFiles": []
|
||||
},
|
||||
{
|
||||
@@ -830,8 +842,12 @@
|
||||
"file": "one-route-many-hand-kept-lists/case/case-eight-places-a-single-route-touches.md",
|
||||
"status": "게시 전",
|
||||
"studioId": "",
|
||||
"assets": [],
|
||||
"assetFiles": [],
|
||||
"assets": [
|
||||
"route-fanout"
|
||||
],
|
||||
"assetFiles": [
|
||||
"route-fanout"
|
||||
],
|
||||
"evidenceFiles": []
|
||||
}
|
||||
],
|
||||
@@ -3026,5 +3042,5 @@
|
||||
"unlisted": 0,
|
||||
"candidates": 91
|
||||
},
|
||||
"ssotSha256": "974abab805e33daa531fa23fde85309c0de17becf6c5e460ef5ff00d057e20c9"
|
||||
"ssotSha256": "ce1a912be009678ebea0099501f63c96dfa11cd7f873c93f14fdddb2c46e3911"
|
||||
}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ source:
|
||||
- **계약은 앵커라고 적었고 만드는 쪽은 경로를 만들었다**
|
||||
같은 앵커 구조에서 난 주소 쪽 사건이다.
|
||||
- **관계의 요약이 경계 세 곳을 지나며 사라졌다**
|
||||
같은 시기에 계약의 빈칸으로 난 다른 사건이다.
|
||||
같은 시기에 계약의 빈칸으로 난 다른 사건이다. 여기서 요약은 공개 계약의 `ResolvedRelation.summary`다.
|
||||
|
||||
## 문제
|
||||
|
||||
|
||||
+9
-2
@@ -7,6 +7,9 @@ topicName: 값이 경계에서 사라진다
|
||||
project: TechLog
|
||||
status: 게시 전
|
||||
lastVerifiedOn: 2026-09-04
|
||||
assets:
|
||||
- key: summary-drop-path
|
||||
file: ../../../final/assets/diagrams/summary-drop-path/summary-drop-path.svg
|
||||
sourceRevision: tech-log@2026-09-02
|
||||
source:
|
||||
- final/document.md#§5.2
|
||||
@@ -15,7 +18,7 @@ source:
|
||||
|
||||
# 관계의 요약이 경계 세 곳을 지나며 사라졌다
|
||||
|
||||
관계 목록의 라벨을 고쳤는데 요약은 여전히 비어 있었다. 한 경계를 고치고 확인했더니 다음 경계가 버리고 있었고, 그것을 고치니 그다음이 버렸다. 세 번째는 계약에 담을 칸 자체가 없었다.
|
||||
공개 relation 목록의 라벨을 고쳤는데 `ResolvedRelation.summary`는 여전히 비어 있었다. 한 경계를 고치고 확인했더니 다음 경계가 버리고 있었고, 그것을 고치니 그다음이 버렸다. 세 번째는 계약에 담을 칸 자체가 없었다.
|
||||
|
||||
## 관계
|
||||
|
||||
@@ -28,7 +31,7 @@ source:
|
||||
|
||||
## 문제
|
||||
|
||||
관계 목록은 한 줄에 대상의 종류와 작성자가 쓴 이유와 대상의 요약을 보인다. 라벨은 고쳤는데 요약 칸이 계속 비어 있었다.
|
||||
공개 relation 목록은 한 줄에 대상의 종류와 작성자가 쓴 이유와 대상의 요약을 보인다. 라벨은 고쳤는데 요약 칸이 계속 비어 있었다.
|
||||
|
||||
계약에는 요약이 있었다. DB 에도 값이 있었다. 화면까지 오지 못했다.
|
||||
|
||||
@@ -63,6 +66,10 @@ tech-log-backend : 92679f5 이후
|
||||
|
||||
## 세 번 버려졌다
|
||||
|
||||
이 그림은 열한 경계 전체가 아니라 이번 사고에서 `summary`가 실제로 끊긴 세 지점만 좁혀 본다.
|
||||
|
||||

|
||||
|
||||
관계 목록의 라벨을 고치고 화면을 봤을 때 요약은 여전히 비어 있었다. 값이 지나는 경계를 하나씩 따라가니 세 곳에서 버려지고 있었다.
|
||||
|
||||
```text
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ source:
|
||||
- **공개 Reference 가 통째로 비어 있었다 — 이름이 어긋났고 본문은 다른 테이블에 있었다**
|
||||
이 경계 중 두 곳에서 값이 사라진 사건이다.
|
||||
- **관계의 요약이 경계 세 곳을 지나며 사라졌다**
|
||||
한 값이 연달아 세 경계에서 버려진 사건이다.
|
||||
한 값이 연달아 세 경계에서 버려진 사건이다. 여기서 관계 요약은 `ResolvedRelation.summary`를 뜻한다.
|
||||
- **한 경계를 고쳤으면 값의 여정 끝에서 확인한다**
|
||||
이 경계 수가 그 규칙의 근거다.
|
||||
|
||||
@@ -51,7 +51,7 @@ PostgreSQL 테이블
|
||||
└─ 화면 컴포넌트
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
저장 쪽에 둘, 백엔드 조립에 넷, 전선에 하나, 프론트엔드 조립에 셋, 화면에 하나다. 저장소 경계로 보면 백엔드가 여섯, 전선이 하나, 프론트엔드가 넷이다.
|
||||
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ source:
|
||||
|
||||
# Studio 에서는 보이는데 공개 쪽만 비면 그 사이에 계약이 있다
|
||||
|
||||
Studio 편집기에서는 값이 다 보이는데 공개 화면만 비어 있으면, 두 화면이 같은 DB 를 보고 있으므로 그 사이의 계약에 칸이 없다. 이 저장소에서 같은 신호가 여덟 번 같은 원인을 가리켰다.
|
||||
Studio 편집기에서는 값이 다 보이는데 공개 화면만 비어 있다면, 작성과 공개 사이의 계약 경계를 먼저 확인할 만한 강한 신호다. 이 저장소에서는 같은 신호가 여덟 번 계약 누락을 가리켰지만, 저장 구조가 종류별로 다른 경우에는 조회 로직이 원인일 수 있다.
|
||||
|
||||
## 관계
|
||||
|
||||
@@ -36,7 +36,7 @@ Studio 편집기에서는 값이 다 보이는데 공개 화면만 비어 있으
|
||||
|
||||
### 1. Studio 에서는 보이고 공개 쪽만 비면 그 사이의 계약을 먼저 본다
|
||||
|
||||
두 화면이 같은 데이터베이스를 보는데 한쪽만 비면, 다른 것은 그 사이에 놓인 계약이다. 작성 쪽은 작성 계약을 지나고 조회 쪽은 조회 계약을 지난다. 이 저장소에서 같은 신호가 여덟 번 같은 원인을 가리켰다.
|
||||
두 화면이 같은 데이터베이스를 보는데 한쪽만 비면, 먼저 두 화면 사이의 계약 차이를 확인한다. 작성 쪽은 작성 계약을 지나고 조회 쪽은 조회 계약을 지난다. 이 저장소에서는 같은 신호가 여덟 번 계약 누락을 가리켰다. 이름과 계약이 맞는데도 값이 비면 종류별 저장 구조와 조회 로직으로 범위를 옮긴다.
|
||||
|
||||
### 2. 화면이 그리는 칸을 먼저 적고 응답에 있는지 하나씩 맞춘다
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ source:
|
||||
## 관계
|
||||
|
||||
- **관계의 요약이 경계 세 곳을 지나며 사라졌다**
|
||||
한 경계를 고치고 판단해 두 번 틀린 사건이다.
|
||||
한 경계를 고치고 판단해 두 번 틀린 사건이다. 여기서 요약은 공개 계약의 `ResolvedRelation.summary`다.
|
||||
- **공개 화면 한 줄이 그려지기까지 값이 지나는 경계 열한 개**
|
||||
왜 중간 확인이 부족한지가 그 개념에 있다.
|
||||
- **TypeScript 가 검사를 놓아 주는 네 곳**
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ tsconfig : 루트가 project references 만 나열
|
||||
| web-worker | 웹 워커 |
|
||||
| service-worker | 서비스 워커 |
|
||||
|
||||
여섯을 따로 두는 이유는 각각 다른 런타임 타입 정의를 쓰기 때문이다. 워커는 DOM 을 갖지 않고 node 는 브라우저 전역을 갖지 않는다.
|
||||
여섯을 따로 두는 이유는 각각 다른 런타임 타입 정의를 쓰기 때문이다. 워커는 DOM(Document Object Model, 문서 객체 모델)을 갖지 않고 node 는 브라우저 전역을 갖지 않는다.
|
||||
|
||||
## 통과가 무엇을 뜻했나
|
||||
|
||||
|
||||
Reference in New Issue
Block a user