리뷰 두 건을 반영했다. 계약 - 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>
12 KiB
AI가 쓴 티
문장 하나하나는 멀쩡한데 문서군 전체가 같은 리듬으로 굴러갈 때 티가 난다. 개별 문장을 일부러 어눌하게 만드는 방향은 역효과다. 아래는 실제 리뷰에서 지적된 것들이다.
이 문서가 문체 규칙의 정본이다. explaining.md 는 무엇을 더 설명할지를 다루고, 둘이 어긋나면
이쪽을 따른다.
설명 뒤에 평가를 붙이지 않는다
가장 많이 지적된 형태다. 기술 설명은 이미 끝났는데 그 뒤에 그 설명을 평가하거나, 다음을 예고하거나, 독자가 오해할 것이라고 가정하거나, 어떻게 읽고 써야 하는지 지시하는 문장이 하나 더 붙는다.
문장마다 무엇을 주는지 본다 — 동작, 정의, 근거, 판단에 영향을 주는 조건 가운데 하나여야 한다. 그 넷 중 어느 것도 주지 않고 판정만 하는 문장은 지운다. 특정 단어를 금지하는 방식은 쓰지 않는다. 「중요하다」가 나쁜 것이 아니라 그 문장이 아무것도 더하지 않는 것이 문제다.
고치는 순서는 내용이 먼저다. 필요 없는 문장을 남긴 채 표현만 자연스럽게 바꾸면 어색한 문장의 표현만 달라진다. 평가·예고·되풀이를 걷어낸 뒤에 문장을 다듬는다.
네 가지가 반복된다. 전부 「다른 표현으로 고치는 사례」가 아니라 **「통째로 지우는 사례」**다.
설명한 것의 중요성을 다시 평가한다
✗ 다른 하나는 앞 단계가 어긋났는데도 나머지를 계속 돌려 마지막에 전체 통과를 남기는 일이다.
그렇게 만든 기록은 경계가 지켜졌다는 근거가 되지 않는다.
○ 다른 하나는 앞 단계가 어긋났는데도 나머지를 계속 돌려 마지막에 전체 통과를 남기는 일이다.
✗ 발행된 SQL에 Limit 노드가 없다는 것 자체가 DB가 페이징을 하지 않았다는 증거다.
○ 발행된 SQL에는 Limit 노드가 없었다.
「증거다」「핵심이다」「너무 넓다」「서로를 대신하지 않는다」로 끝나는 꼬리 문장이 이 형태다. 앞 문장이 사실을 말했으면 거기서 끝낸다.
독자가 오해할 것이라고 먼저 가정한다
✗ 훑은 행만 보면 keyset이 결과까지 줄인 것으로 짐작하기 쉽다. 커서로 넘긴 두 번째 페이지는
OFFSET의 두 번째 페이지와 같은 20개 식별자를 같은 순서로 반환했다.
○ 커서로 넘긴 두 번째 페이지는 OFFSET의 두 번째 페이지와 같은 20개 식별자를 같은 순서로 반환했다.
✗ Web Storage에 토큰을 쓰지 않으니 JavaScript에서도 토큰이 사라진다고 읽기 쉽다. 그렇지 않다.
액세스·리프레시·ID 토큰은 실행 중 메모리에 있다.
○ 액세스·리프레시·ID 토큰은 실행 중 메모리에 있다.
「~로 읽기 쉽다」「~라고 생각하면 안 된다」「~로 보기 쉽다」. 자료에 누군가 실제로 그렇게 읽었다는 기록(버그·정정·문의)이 없으면 독자를 지어낸 것이다. 관측부터 적으면 오해는 생기지 않는다.
어떻게 읽고 어떻게 써야 하는지 지시한다
✗ AP2와 AP3의 쿠키는 서버 쪽 상태를 찾는 열쇠이고, AP4의 쿠키는 최소 상태를 담은 값이다.
두 쿠키를 같은 문장으로 설명하면 서버 저장소가 있는 쪽과 없는 쪽이 구분되지 않는다.
○ AP2와 AP3의 쿠키는 서버 쪽 상태를 찾는 열쇠이고, AP4의 쿠키는 최소 상태를 담은 값이다.
✗ 이 두 낱말만 알면 따라올 수 있고, 나머지는 처음 나오는 곳에서 푼다.
✗ 먼저 브라우저가 들고 있는 값부터 보고, 그 값이 Bearer 요청이 되기까지를 따라간다.
✗ native로 내려갔다는 것과 그 범위를 함께 적는다.
「~라고 설명하면 ~가 구분되지 않는다」「~를 함께 적는다」「먼저 ~를 보고 다음에 ~를 본다」. 독자에게 필요한 것은 각 쿠키가 무엇을 보관하는지이지 그것을 어떻게 설명해야 하는지가 아니다. 이런 문장은 작성자의 검토 메모다. 차례 예고도 같다 — 다음 절이 바로 시작하면 된다.
이미 설명한 것을 다시 말한다
✗ 배치는 SQL 왕복 횟수를 줄이고 프로젝션은 적재할 대상을 줄인다. 두 효과는 서로를 대신하지 않는다.
○ 배치는 SQL 왕복 횟수를 줄이고 프로젝션은 적재할 대상을 줄인다.
✗ 반복이 사라진 것이 아니라 스트림 뒤로 숨었다.
○ 현재 매핑에서는 각 아이템의 getHighlights()에 접근하면서 지연 로딩이 실행된다.
같은 대조를 추상어로 한 번 더 하거나, 동작 설명 뒤에 인상적인 문장으로 닫는 것. 동작을 그대로 적으면 독자가 비유를 코드 동작으로 다시 번역하지 않아도 된다.
SSOT 에 같은 문장이 있어도 옮기지 않는다
final/document.md 는 사실과 근거의 기준이지 문장의 기준이 아니다. 원문에 「너무 넓은 성공 기준입니다」가
있어도 기록에 옮길 이유는 없다. 옮기는 것은 수치·조건·동작·판단이고, 평가는 옮기지 않는다.
무엇을 남기나
- 코드가 그렇게 동작하는 이유, 측정 조건, 결과를 읽는 데 필요한 예외
- 실제 선택을 바꾼 판단 — 「200 이어도 정상인 이유는 세션이 유효하기 때문이다」처럼 판정에 영향을 주는 것
- 요약·결론·본문 사이의 반복 — Studio 칸 구조상 필요하다. 걷어낼 것은 한 칸 안에서 설명 직후에 붙은 문장이다
어미 수·절 수·안내 문장 수 같은 수치는 참고 정보다. style_profile.mjs 가 「벗어남」을 내도 그것을
맞추려고 문장을 넣지 않는다. 위 첫 예시는 문장 검사를 error 0 으로 통과한 채로 지적됐다.
억지 구어체를 만들지 않는다
AI 티를 지우려고 넣은 질문체와 청유형이 오히려 「AI 문장을 억지로 인간화한 것」으로 읽힌다. 제목은 명사구로 두고, 본문은 무엇을 봤는지로 시작한다.
| 억지로 사람처럼 | 그냥 제목 |
|---|---|
무엇이 서버로 책임 이전을 했지? |
서버로 옮겨진 책임 |
AP2_SESSION은 언제 생기지? |
AP2_SESSION이 생성되는 시점 |
one-time handoff인가..? |
/token/access는 일회성 전달이 아니다 |
memory-only가 위험을 막아주나..? |
memory-only가 줄이는 위험 |
세 겹으로 나눠서 막아보자 |
세 개의 독립된 경계 |
그래서 이 패턴의 문제는 받은 헤더를 어떻게 믿지? |
upstream은 헤더의 출처를 구분할 수 없다 |
..?, ~하지?, ~해보자, ~하나?, 확인하자가 보이면 지운다.
결론을 먼저 정리하지 않는다
관측한 사실이 결론을 만들게 둔다. 정리된 대구 문장은 한 문서에 한 번이면 충분하다.
✗ BFF는 token을 브라우저에서 제거하는 대신 session과 CSRF 책임을 갖게 된다.
○ 브라우저 network에서 token endpoint 호출과 Authorization Bearer가 사라졌다.
대신 `/bff/api/me` 요청에는 AP3_SESSION이 자동으로 붙었다.
상태 변경 요청을 추가하면서 이 cookie 때문에 CSRF 검증이 필요해졌다.
Case는 튜토리얼이 아니라 사건의 순서를 따라간다.
처음 예상 → 실제 요청·코드에서 본 것 → 예상과 달랐던 지점 → 왜 그런지 → 확인한 범위
「처음에는 ~라고 봤다. 그런데 ~를 따라가 보니 ~였다」는 실제로 그렇게 생각한 기록이 있을 때만 쓴다. 없으면 지어낸 1인칭이다.
같은 문형을 문서마다 되풀이하지 않는다
한 문서군에서 아래 두 구조가 반복되면 그것 자체가 티다.
A를 얻는다. 대신 B를 내준다. 그래서 C를 해야 한다.
A와 B는 다르다. 둘을 나눠야 한다. 같은 이름으로 부르면 안 된다.
다 쓰고 나면 세어 본다. 대신·그래서·함께·그대로·따로·하게 된다·정해야가 문서마다
비슷한 횟수로 나오면 문형이 굳은 것이다.
grep -o '대신\|그래서\|함께\|그대로\|따로\|하게 된다' *.md | sort | uniq -c | sort -rn
길이를 고르게 맞추지 않는다
규칙 11개를 같은 길이로 쓰면 사람이 고른 것으로 읽히지 않는다. 중요한 규칙은 길게 쓰고 자명한 규칙은 한 줄로 끝낸다. 항목이 축으로 정리되는 내용이면 산문 대신 표 하나가 낫다.
endpoint | 브라우저가 접근 | credential | secret | 검증 주체
표로 정리한 뒤 특이사항만 문장으로 쓴다.
Question은 균형 잡힌 비교표가 아니다
선택지마다 「장점. 대신 단점.」을 똑같이 붙여 놓으면 아직 모르는 문제가 아니라 비교를 요청받고 답한 문서가 된다. 실제 설계 기록은 이렇게 생겼다.
지금 확인한 사실
지금 모르는 것
유력한 후보와 그 후보에서 확인할 항목
제외한 후보와 제외한 이유
무엇으로 결정할지
후보를 균등하게 나열하는 대신 지금 위치에서 한 단계 앞의 결정만 본다. 그리고 저장소 선택과 구조 변경처럼 층이 다른 선택지는 같은 목록에 넣지 않는다.
검증 전 결과를 결론으로 쓰지 않는다
Question이 재현하지 않은 일을 단정하면 답을 이미 아는 문서가 된다.
✗ 두 replica가 같은 refresh token으로 동시에 갱신하면 한쪽은 거부되게 된다.
○ 두 replica가 같은 refresh token으로 동시에 갱신할 수 있다. rotation 정책 때문에 두 번째
사용이 거부될 가능성이 있고, 실제 응답과 session 영향은 아직 재현하지 않았다.
제약에 「이 전제는 바꾸지 않는다」고 써 놓고 그 전제를 바꾸는 선택지를 나란히 두지 않는다. 비교용으로 남기려면 「제약상 제외」로 따로 뺀다.
가짜 정량성을 만들지 않는다
측정할 수 없는 것을 숫자처럼 쓰지 않는다.
✗ 헤더 계약 수가 BFF 계약 수를 넘는 지점이 되돌릴 기준이다.
○ 전달하려는 claim이 계속 늘어나는가. role·tenant 변경이 즉시 반영돼야 하는가.
정책이 애플리케이션 도메인을 알아야 하는가.
범위를 넓히는 단정을 쓰지 않는다
| 넓힌 것 | 좁힌 것 |
|---|---|
| token이 memory 밖으로 나가는 유일한 구간 | 현재 SPA 코드에서 access token이 외부 요청으로 나가는 지점 |
| 짧은 수명이 사실상 유일한 방어 | 이 구성에는 denylist도 introspection도 없다. 그래서 노출 시간을 줄이는 주된 수단이 짧은 TTL이다 |
| 배포 한 번에 전원이 로그아웃된다 | 상태가 process-local이라 그 instance가 종료되면 그 instance가 들고 있던 session과 authorized client가 사라진다 |
| JDBC는 컬럼 암호화 수단이 대부분 이미 갖춰져 있다 | (환경마다 다르다. 확인한 것만 쓴다) |
「~하면 ~을 우회할 수 있다」도 실제 구성에서 확인한 범위까지만 쓴다.
기록되지 않은 과거를 만들지 않는다
~하던 관행을 버리게 된다, 그동안 ~라고 불러 왔다. 그런 이력이 자료에 없으면 지운다.
Reference는 Case의 재설명이 아니다
같은 프로젝트의 Case를 문장만 바꿔 옮기면 규칙 수만 늘어난다. 다른 프로젝트에서 다시 적용할 수 있는 기준만 남기고, 사건은 Case에 두고 관계로 가리킨다. 규칙이 10개를 넘으면 축이 겹치는지 본다.
현재 검증과 운영 권고를 섞지 않는다
secret manager, network policy, mTLS처럼 지금 구성에 없는 것을 규칙에 그냥 적으면 Best Practice를 덧붙인 문서가 된다. 두 묶음으로 나눈다.
현재 확인한 것
운영에서 추가로 필요한 것
테스트가 무엇을 단정하는지 쓴다
✗ 이 요청이 200을 받는지 아닌지는 중요하지 않다.
○ 이 테스트의 assertion은 status code가 아니다. 정상 session을 함께 보냈으니 요청은 200이
될 수 있다. 확인할 값은 응답의 `user`가 `spoofed-admin`으로 바뀌지 않았는지다.
설정 → 실제 요청 → 실제 status를 잇는 문장을 늘리고 일반론을 줄인다.