리뷰 두 건을 반영했다. 계약 - tech-log-tree.json 하나가 분해 계약이자 색인이다. 사람이 읽는 트리·Node Specification· 후보 대장은 없어졌고, 문서에 남아 있던 그 개념을 걷어냈다 - candidateScope — 후보를 찾는 SSOT 범위. 접어 넣은 제2부·제3부는 근거이지 후보가 아니다 - sourceRepository — 분석한 저장소의 경로·리비전·판단 근거. 리비전을 모르면 null 로 두고 지어내지 않는다. 갈래가 여럿이면 revisions - 검사기: 계약 미채택·PENDING·PROMOTE↔글감 양방향·candidateScope·sourceRepository 를 error/warn 으로 센다. 옛 스키마도 검사를 피하지 못한다. 테스트 22 → 31 기록 쓰기 - 템플릿 5종에 source·sourceRevision·topicName, Question 에 닫는 조건, 본문 없는 종류에서 assets 제거. 고정 절 개수 삭제 - check_evidence.mjs — 인용한 코드가 SSOT 에 있는지, 앵커가 SSOT 를 가리키는지, 제목이 계약과 같은지, 리비전이 저장소에 있는지. 게시된 기록에서 SSOT 와 다른 URL 을 잡았다 문체 - 문체 규칙의 정본을 ai-tells.md 로. explaining.md 의 질문체 제목·절 끝 대조 반복·그림 예고 규칙을 삭제해 충돌을 없앴다. 첫 절 「설명 뒤에 평가를 붙이지 않는다」에 지우는 사례 네 유형 - voice 스킬의 「독자 쪽을 본다」를 자료에 오독 기록이 있을 때로 좁히고, 평가만 더한 예시를 교체 - check_prose: 안내 문장을 요구하던 경고 제거, 문장이 끝나지 않은 채 문단이 끝나는 조각 검사 추가 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
19 KiB
설명의 깊이와 말투
가장 자주 나오는 지적은 설명이 짧다는 것이다. 사실은 맞는데 독자가 따라오지 못한다.
그 반대도 같은 무게로 지적된다 — 설명이 끝난 뒤에 그 설명을 평가하거나, 다음 절을 예고하거나,
독자가 오해할 것이라고 가정하는 문장이 붙는 것. 이 문서는 무엇을 더 설명하는가를 다루고,
무엇을 빼는가는 ai-tells.md 첫 절이 다룬다. 문체 규칙의 정본은 ai-tells.md 다. 두 문서가
어긋나면 그쪽을 따른다. 여기 규칙은 전부 「그 설명이 없으면 독자가 막히는 자리」에서만 쓴다.
이미 설명된 문단에 더하지 않는다.
이름을 댔으면 왜 있는지도 댄다
낯선 클래스·기법·설정 이름을 적고 다음 문장으로 넘어가지 않는다. 왜 그것이 존재하는지를 한 문장 붙인다.
쓰지 않는다
XorCsrfTokenRequestAttributeHandler가 token을 XOR와 Base64로 가린다.
쓴다
XorCsrfTokenRequestAttributeHandler가 token을 XOR와 Base64로 가린다.
BREACH 공격을 줄이기 위해서다. HTTP 응답 압축 크기의 차이로 응답 안의 비밀값을 조금씩
추측하는 공격이고, 그래서 응답에 실리는 값을 매번 다르게 만든다.
깊게 안 갈 것이면 한 문장 요약만 준다. 「여기서 깊게 다루지는 않는다」 같은 예고는 붙이지 않는다 — 요약이 있으면 그것으로 충분하고, 없으면 독자는 그 자리에서 검색하러 나간다. 이 규칙은 처음 나오는 낯선 이름에만 걸린다. 이미 설명한 이름이나 문맥에서 분명한 이름에는 붙이지 않는다.
「역할이 다르다」로 끝내지 않는다
두 값이 왜 하나로 합쳐질 수 없는지 메커니즘을 적는다.
쓰지 않는다
첫 줄은 claim 검증 기준이고 셋째·넷째 줄은 network 경로다. 역할이 다르다.
쓴다
issuer는 요청을 보내는 주소가 아니라 발급된 token의 iss claim이 기대한 값과 같은지
확인하는 기준값이다. token URL과 userinfo URL은 실제로 요청을 보내는 내부 주소다.
브라우저는 docker 내부 호스트명에 접근할 수 없어 로그인에는 외부 주소를 쓰고,
컨테이너는 자기 localhost가 그 서버가 아니므로 내부 통신에는 service 이름을 쓴다.
값이 합쳐지면 합쳐진 결과를 보여 준다
두 곳에서 온 값이 하나의 요청이 되는 흐름은 조립된 실물까지 보여 준다. 대응표만 두면 독자가 머릿속으로 조립해야 한다.
대응표만 두지 않는다
body.token = masked token
cookie XSRF-TOKEN = raw token
POST X-XSRF-TOKEN = same raw token
조립된 요청을 이어서 보여 준다
POST /bff/theme HTTP/1.1
Content-Type: application/json
Cookie: SESSION=abc123; XSRF-TOKEN=xyz789
X-XSRF-TOKEN: xyz789
직접 본 것은 따로 절을 만든다
테스트 계약을 인용하는 것과 직접 열어서 본 것은 다른 증거다. 화면을 열어 확인했다면 그 사실을 따로 적는다.
로그인 뒤 브라우저 개발자 도구에서 요청과 저장소를 확인했다. Keycloak token endpoint를
직접 호출하지 않았고 Resource Server 포트도 직접 호출하지 않았다. localStorage와
sessionStorage에도 accessToken과 refreshToken이 없었다.
확인하지 않았으면 쓰지 않는다. 계약 인용은 계약 인용이라고 적는다.
내부 코드명을 산문에 쓰지 않는다
독자는 AP1~AP4 같은 내부 번호를 모른다. 산문에서는 구조 이름으로 부른다.
| 쓰지 않는다 | 쓴다 |
|---|---|
| AP1은 | SPA 구조는 |
| AP2가 맞는 경우는 | Mediator를 고를 수 있다 |
| AP3은 | BFF에서는 · BFF 구조에선 |
| AP4는 | OAuth2-Proxy 구조는 |
번호는 표의 축과 식별자에만 남긴다 — AP1~AP3 | AP4 같은 비교 열, AP3_SESSION 같은
실제 값. slug에도 넣지 않는다.
쓰지 않는다 ap4-identity-header-trust
쓴다 identity-header-trust
굵게를 걷어낸다
**굵게**는 거의 쓰지 않는다. 한 절에 하나를 넘기면 강조가 아니라 얼룩이 된다. 강조는
자리로 한다 — 절을 따로 떼거나, 표에서 그 행을 첫 줄에 두거나, 짧은 문단으로 끊는다.
단정을 좁힌다
「항상 그렇다」로 적기 전에 예외를 센다.
틀렸다 token endpoint는 server-to-server 호출이다
맞다 server-to-server 호출일 수도 있고 browser-to-server 호출일 수도 있다
public client는 브라우저가 직접 token endpoint를 부른다. 한 구조에서 본 것을 protocol 전체의 성질로 넓히지 않는다.
흐름은 끊지 않고 이어 간다
한 흐름은 한 문단으로 이어 간다. 이후 · 그리고 · ~하면 · ~한 뒤로 다음 단계를 붙인다.
단계마다 문장을 끊고 각각 결론을 다는 방식은 쓰지 않는다.
쓰지 않는다
브라우저가 로그인 요청을 보낸다. mediator가 세션을 확인한다. Keycloak으로 302를 준다.
브라우저가 URI를 조립해 Keycloak을 부른다. code를 받는다. mediator가 교환한다.
쓴다
브라우저가 로그인 요청을 보내면 mediator에서 세션을 확인한 뒤 Keycloak으로 302
리다이렉트를 하게 된다. 그리고 브라우저가 로그인 요청과 관련된 값을 조립해 URI를 만들고
Keycloak으로 요청을 보낸다. 이후 로그인 화면에서 아이디와 비밀번호를 넣어 전달하면
authorization code와 함께 redirect되고, 그 code를 mediator가 token으로 교환하게 된다.
짧은 문장을 나열하면 각 문장이 다 결론처럼 읽힌다. 읽는 사람은 어디가 흐름이고 어디가 판단인지 구분하지 못한다.
앞 구조와 무엇이 달라졌는지로 연다
절을 열 때 이전 구조를 먼저 세우고 무엇이 옮겨졌는지 말한다. 그러면 비교 축이 문단 안에서 고정된다.
앞선 구조에서는 브라우저가 token 교환과 관리, API 요청까지 전부 맡았다.
이 구조에서는 token 관리와 교환의 위치가 브라우저에서 Spring backend로 옮겨지게 된다.
결론은 문장 끝에 붙인다
한 줄짜리 단정문을 따로 떼어 강조하지 않는다. 앞 문장에서 그래서 · 그렇기 때문에로
이어 간다.
쓰지 않는다
code 교환과 API 호출을 둘 다 server가 대신해야 한다. 하나만 옮겨서는 안 된다.
원문이 응답 본문과 지역 변수와 헤더를 지난다. 세 자리 모두 같은 실행 영역이라
「브라우저에 token 없음」을 만족하지 못한다.
쓴다
refresh token만 옮기는 구조에서는 브라우저가 Resource Server를 직접 부르기 때문에
access token이 필요하고, 그것을 응답 본문으로 받게 된다. 그래서 원문이 응답 본문과
지역 변수, Authorization 헤더를 차례로 지나게 되고 결국 브라우저에 token이 없다고
말할 수 없게 된다.
같은 말을 다시 말해 강조하지 않는다. 앞 문장이 이미 말했으면 거기서 끝낸다.
하나만 옮겨서는 안 된다 같은 덧붙임이 그것이다.
~하게 된다는 상태가 실제로 바뀌는 자리에만 쓴다
무엇이 바뀌는 대목에서는 ~한다보다 ~하게 된다가 맞다. 흐름을 따라가는 자리이기
때문이다.
| 쓰지 않는다 | 쓴다 |
|---|---|
| 위치가 바뀐다 | 위치가 옮겨지게 된다 |
| mediator가 code를 교환한다 | mediator가 code를 token으로 교환하게 된다 |
| session cookie를 발급한다 | 저장이 끝나고 session cookie를 발급하게 된다 |
정의·분류·사실·지시에는 붙이지 않는다. 어미만 바꾸면 문장이 어색해지고 무엇이 흐름이고 무엇이 기준인지도 흐려진다.
| 자리 | 틀렸다 | 맞다 |
|---|---|---|
| 정의 | 네 구조는 서로 다른 운영 계약이 된다 | 운영 계약이다 |
| 분류 | 이 흐름은 별도 client가 된다 | 별도 client다 |
| 사실 | upstream은 role 판단을 하지 않게 된다 | 하지 않는다 |
| 지시 | edge에 인증을 맡기지 않게 된다 | 맡기지 않는다 |
가릴 때는 정말로 무엇이 되는지를 묻는다. refresh token이 무효가 된다, lock 자체가 새 장애 지점이 된다는 상태가 바뀌므로 맞다. 배치가 된다는 원래 배치였으므로 틀렸다.
Reference의 규칙 제목과 Decision의 결정문도 ~한다로 끊는다. 그 자리는 기준이다.
번역투를 걷어낸다
가장 자주 나오는 지적 두 번째다. 어미는 한국어인데 문장 구조가 영어여서 읽기 힘들다.
~하는 것은 ~이다를 쓰지 않는다
영어의 What matters is …를 그대로 옮긴 구조다. 한국어는 동사로 바로 간다.
| 쓰지 않는다 | 쓴다 |
|---|---|
| 고르기 전에 답할 것은 세 가지 배치다 | 고르기 전에 누가 code를 바꾸고 누가 token을 드는지부터 정한다 |
| 실제로 갈리는 것은 보안 수준이 아니다 | 보안 수준으로는 구조가 갈리지 않았다 |
| 요구로 들어오면 남는 것은 BFF다 | 요구로 들어오면 BFF만 남는다 |
| 여기서 줄어드는 것은 재사용 반경이다 | 여기서는 재사용 반경만 줄어든다 |
지시대명사를 주어로 세우지 않는다
그것이 · 이것이로 문장을 시작하면 앞 문장을 되짚어야 읽힌다.
| 쓰지 않는다 | 쓴다 |
|---|---|
| 그것이 더 안전한 순서는 아니다 | 그렇다고 뒤로 갈수록 더 안전해지지는 않는다 |
| 그것이 학습 환경임을 문서에 남긴다 | 학습 환경이라고 문서에 적어 둔다 |
| 정책상 그것이 금지라면 | 정책상 브라우저 token이 금지라면 |
~라는 뜻은 아니다 · ~는 것은 아니다를 쓰지 않는다
부정을 두 겹으로 쌓지 않는다. 긍정으로 뒤집고 조건을 붙인다.
| 쓰지 않는다 | 쓴다 |
|---|---|
| confidential이라고 token이 안 가는 것은 아니다 | confidential이어도 token이 갈 수 있다 |
| client 인증이 있다고 PKCE가 필요 없어지는 것은 아니다 | client 인증이 있어도 PKCE는 여전히 쓸모가 있다 |
| 도메인 정책을 통과했다는 뜻은 아니다 | 도메인 정책은 아직 통과해 보지 않았다 |
| 이름이 장애 복구를 갖췄다는 뜻은 아니다 | 이름만 봐서는 장애 복구가 갖춰졌는지 알 수 없다 |
추상 공간 은유를 쓰지 않는다
그 자리에 무엇이 들어오는지 같은 표현은 실체가 없다. 무엇을 누가 하는지로 바꾼다.
쓰지 않는다 token을 없앨 때 그 자리에 무엇이 들어오는지 적는다
쓴다 token을 없앤 대신 무엇을 관리해야 하는지 적는다
쓰지 않는다 CSRF와 공유 저장소가 그 자리에 들어오게 된다
쓴다 server로 옮기면 server session과 CSRF, 공유 저장소를 관리해야 한다
비유로 설명하지 않는다
기록하는 글이지 수필이 아니다. 그림이 떠오르는 표현을 쓰면 읽는 사람마다 다르게 읽는다.
| 쓰지 않는다 | 쓴다 |
|---|---|
| 얻은 것 옆에 내준 것을 같이 둔다 | 얻은 것과 내준 것을 함께 적는다 |
| 판단 자료가 아니라 홍보문이 되어 버린다 | 무엇을 감수해야 하는지 알 수 없다 |
| 그 자리를 PKCE가 메운다 | secret 대신 PKCE를 쓴다 |
| 짧은 수명이 그 자리를 대신한다 | token 수명을 짧게 두는 것이 유일한 방어가 된다 |
| BFF가 떠안게 된다 | BFF가 맡아야 한다 |
| session ID마다 독립된 금고 | session ID마다 token을 따로 보관 |
| 401을 그대로 흘리면 | 401을 그대로 내려보내면 |
| 재사용되는 반경 | 재사용될 범위 |
| 그 뒤 검사가 다 무의미해진다 | 그 뒤에 무엇을 검사해도 소용이 없다 |
| 값이 새면 | 값이 유출되면 |
| 덮어쓰기가 도는지 | 덮어쓰기가 실제로 동작하는지 |
| 인스턴스가 죽으면 | 인스턴스가 내려가면 |
| 경쟁이 자연히 흡수된다 | 경쟁이 저절로 해소된다 |
| 통째로 건너뛰어진다 | 함께 빠지게 된다 |
| 다른 사람이 되어 버린다 | 다른 사람으로 인식된다 |
| token을 치운다 | token을 없앤다 |
| 배치가 구조를 가른다 | 어디에 두느냐에 따라 구조가 달라진다 |
| 비교표의 축에 끼어든다 | 성격이 다른 항목이 섞인다 |
자리 · 옆 · 칸 · 축 같은 공간 말, 떠안다 · 죽다 · 흡수하다 같은 의인·비유,
무의미해진다 · 되어 버린다 같은 과장을 지운다. 지우고 나면 무슨 일이 일어나는지만
남는다.
그 밖에 자주 나오는 것
~에 대한→ 조사로 푼다.학생들에 대한 관심→학생에게 관심이 많다~에 있어서→~에서·~할 때~에 의해·~로 인해→~ 때문에·~가- 피동 → 능동.
결정이 내려졌다→결정했다 ~들→ 복수가 문맥으로 분명하면 뺀다
의미를 바꾸지 않는 단어를 먼저 지운다. 글자 수를 줄이는 것보다 이것이 앞선다.
읽는 법을 지시하지 않는다
「무엇을 막는지를 좁혀서 봐야 한다」, 「여기까지다」, 「먼저 본다」, 「~로 읽으면 안 된다」, 「두 질문을 따로 답한다」. 전부 독자에게 읽는 방법을 알려 주는 문장이다. 문서는 대상을 설명하지 독자의 읽기를 지시하지 않는다.
지울 자리를 찾는 법은 간단하다. 그 문장을 빼도 남은 내용이 그대로면 곁가지다.
✗ 무엇을 막는지를 좁혀서 봐야 한다. code를 누가 훔쳐 가도 verifier가 없으면
token으로 바꾸지 못한다. 여기까지다.
○ code를 누가 훔쳐 가도 verifier가 없으면 token으로 바꾸지 못한다.
앞 문장이 빠져도 뒷 문장의 뜻은 하나도 줄지 않는다. 「먼저 본다」도 마찬가지다. 순서를 지시하는 대신 왜 그런지를 쓴다.
✗ 여기서 걸리면 나머지 비교가 필요 없어지기 때문에 먼저 본다.
○ 이 조건에 걸리면 다른 항목은 볼 필요가 없다.
규칙 제목이 이미 말한 것을 본문에서 다시 지시하는 것도 같은 문제다. 제목이 「얻은 것과 내준 것을 함께 적는다」면 본문 끝에 「함께 적는다」를 또 쓰지 않는다.
동작을 압축하지 않는다
싣는다, 낸다, 친다, 짠다처럼 한 글자로 줄인 동사는 무엇을 어디로 하는지를 지운다. 실제
동작으로 풀어 쓰고, 목적어를 빼지 않는다.
| 압축 | 푼 것 |
|---|---|
| code_challenge를 싣고 | code_challenge를 담아서 보내고 |
| code_verifier를 낸다 | code_verifier를 보낸다 |
| secret을 함께 낸다 | secret을 함께 보낸다 |
| 401을 낸다 | 401을 돌려준다 |
| 요청마다 DB를 친다 | 요청마다 DB를 조회한다 |
| 같다고 가정하고 짜면 | 같다고 가정하고 구현하면 |
| 교환이 끝난다 | 토큰 교환이 된다 |
교환이 끝난다는 무엇의 교환인지가 없다. 앞 문장에서 짐작할 수 있어도 그 자리에 다시 쓴다.
앞에서 말한 것을 다시 짚는다
둘이, 셋이, 그 둘은은 무엇을 가리키는지 독자가 되짚게 만든다. 대명사 대신 세어서 가리키거나
이름을 다시 쓴다.
✗ 둘이 맞아야 교환이 끝난다.
○ 이 두 값이 일치해야 토큰 교환이 된다.
✗ upstream이 받는 요청에서는 둘이 구분되지 않는다.
○ upstream이 받는 요청에서는 이 두 헤더가 구분되지 않는다.
가리키는 대상이 바로 앞 문장에 있어도 마찬가지다. 문장 하나만 떼어 읽어도 뜻이 서는 쪽을 고른다.
한 문장에 동작을 두 개 넣지 않는다
확인하는 동작과 그 결과는 서로 다른 일이다. 한 문장에 이어 붙이면 조건과 결론이 뭉개진다.
✗ 처음 요청에 code_challenge를 싣고, 교환할 때 원본인 code_verifier를 낸다. 둘이 맞아야
교환이 끝난다.
○ 처음 요청에 code_challenge를 담아서 보내고, 교환할 때 원본인 code_verifier를 보내서
이 두 값이 일치하는지 확인한다.
두 값이 일치해야 토큰 교환이 된다.
못 하는 것이 아니라 할 것을 쓴다
「~라고 말할 수는 없다」, 「~를 확인한 것은 아니다」, 「~라고 단정하면 안 된다」로 문단을 열면 독자가 할 일을 스스로 뽑아내야 한다. 무엇이 문제인지, 무엇을 하면 되는지, 어떻게 확인하는지 순서로 쓴다.
✗ wildcard allowlist는 학습 환경에서 편하다. 다만 그것으로 exact callback만 허용하는
가드레일을 확인했다고 말할 수는 없다.
허용 범위가 넓으면 같은 호스트의 다른 경로로 code를 흘릴 여지가 생긴다. 잘못된 redirect를
거부하는지 확인하는 검사도 따로 둔다.
○ wildcard allowlist는 학습 환경에서 편하다. 다만 허용 범위가 넓으면 같은 호스트의 다른
경로로도 code가 갈 수 있다.
실제로 쓰는 callback 주소만 등록해 두면 code가 도착할 수 있는 곳이 그 하나로 줄어든다.
등록하지 않은 redirect_uri를 보냈을 때 거부하는지 확인하는 검사도 따로 둔다.
고친 쪽은 문단이 셋으로 나뉜다. 첫 문단은 무엇이 문제인지, 둘째는 무엇을 하면 되는지, 셋째는 어떻게 확인하는지다. 원래 글은 이 셋이 두 문장 안에 뭉쳐 있어서 가운데 「그래서 좁힌다」가 빠져 있었다.
「여지가 생긴다」도 같이 지운다. 무엇이 어디로 가는지 그대로 쓰면 된다.
| 돌려 말한 것 | 그대로 말한 것 |
|---|---|
| code를 흘릴 여지가 생긴다 | 다른 경로로도 code가 갈 수 있다 |
| 확인한 것은 아니다 | (무엇이 실제로 일어나는지) |
| 단정하면 안 된다 | (그렇게 하려면 무엇이 필요한지) |
| token이 없다고 말할 수 없게 된다 | 브라우저에 token이 남는다 |
부정형이 규칙의 논지 자체일 때는 그대로 둔다. 「network 격리와 헤더 검증을 서로 대신하지 않는다」는 대체할 수 없다는 것이 규칙이고, 본문도 「두 가지를 다 둔다」로 끝난다.
문장 끝
요 · 습니다가 아니라 한다 · 이다로 끝낸다. 그 밖에는 이렇게 쓴다.
~하면 된다를 쓰지 않는다. 조언하는 말투이지 기록하는 말투가 아니다.정하면 된다→정한다,적으면 된다→적는다,두면 된다→둔다A는 B다보다A는 B라는 점이 문제가 된다— 무엇이 걸리는지까지 말한다- 명령조를 줄인다. 검사 항목이 아니라 같이 읽는 사람의 말로 쓴다