# AI가 쓴 티 문장 하나하나는 멀쩡한데 문서군 전체가 같은 리듬으로 굴러갈 때 티가 난다. 개별 문장을 일부러 어눌하게 만드는 방향은 역효과다. 아래는 실제 리뷰에서 지적된 것들이다. ## 억지 구어체를 만들지 않는다 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`를 잇는 문장을 늘리고 일반론을 줄인다.