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