
기계적 패턴 게이트: 반복되는 리뷰 코멘트를 스캔으로 전환한 방법
요약
반복되는 코드 리뷰 코멘트를 자동화된 스캔 스크립트로 전환하여 리뷰의 일관성을 높이는 방법을 다룹니다. 정교한 기술보다는 Bash와 정규 표현식을 활용해 수동 리뷰의 피로도를 줄이고 강제성을 부여하는 실용적인 접근법을 제시합니다.
핵심 포인트
- 반복되는 리뷰 코멘트를 스크립트로 자동화하여 일관성 유지
- Bash와 정규 표현식을 활용한 단순하지만 강력한 도구 구축
- 에이전트의 보고에 의존하지 않는 물리적 스캔 게이트 구축
- 리뷰어의 컨디션에 상관없는 일정한 검토 기준 확립
지난 글에서 저는 리뷰 게이트(review gate)를 에이전트의 통제 밖으로 옮겼습니다. 훅(hook)은 문장이 아니라 파일을 읽습니다. 스캔이 디스크에 아티팩트(artifact)를 작성하지 않으면, 에이전트가 스캔을 실행했다고 아무리 자신 있게 보고하더라도 GitHub에 리뷰를 게시할 수 없습니다. 이를 통해 강제성(enforcement) 문제를 해결했습니다. 하지만 더 근본적인 질문이 해결되지 않은 채 남아 있었습니다. 과연 스캔은 실제로 무엇을 찾고 있는가?
다섯 가지입니다. 이 글은 그 다섯 가지와, 각 항목이 제가 계속 다시 타이핑해야 하는 코멘트에서 저 대신 그것을 다시 타이핑해 주는 스크립트(script)로 변한 작고 약간은 창피한 순간들에 관한 이야기입니다.
이 중 어느 것도 영리하지 않다
미리 말씀드리고 싶은 것은, 이런 종류의 글을 쓸 때 엔지니어링이 실제보다 더 어렵게 들리게 만들고 싶은 유혹이 있지만, 그렇지 않았다는 점입니다. 어렵지 않았습니다. 이 스캔들의 대부분은 몇 줄의 bash와, 실제 엔지니어에게 보여준다면 먼저 사과부터 해야 할 정도의 정규 표현식(regex)일 뿐입니다.
가치는 결코 정교함에 있지 않았습니다. 가치는 반복에 있습니다. 스크립트는 매번, 모든 diff에 대해 동일한 방식으로 동일한 스캔을 실행합니다. 저는 그렇지 않습니다. 저는 화요일 아침에는 주의 깊게 읽지만, 목요일 오후 점심시간에 늦은 사람처럼 읽기도 합니다. 이 두 리뷰 사이의 간극은 수동태 정의를 잡아내는 것과, 그것을 개발자에게 전달하여 개발자가 직접 해석하게 만드는 것 사이의 간극이었습니다. 스캔에는 '목요일 오후'가 없습니다. 그것이 이 모든 제안의 핵심입니다.
이 다섯 가지를 만들어낸 패턴은 다음과 같습니다. 저는 PR(Pull Request)에 리뷰 코멘트를 남깁니다. 그러고 나서 다음 주에 다른 사람의 PR에 거의 동일한 단어로 같은 코멘트를 남깁니다. 문장을 세 번째 타이핑할 때쯤이면, 당신은 그 문장이 스크립트가 되고 싶어 한다는 의심을 하기 시작할 것입니다. 이 스캔들은 모두 제가 쓰는 데 지쳐버린 리뷰 코멘트일 뿐입니다.
다섯 가지 스캔
진짜 스캔들은 style-gate.sh라는 스크립트 안에 들어 있습니다. 각 스캔은 diff(차이점)에서 추가된 산문(prose) 라인을 대상으로 실행되며, 코드와 링크 구문은 먼저 제거되므로 스캔은 오직 산문만을 보게 됩니다. 다음은 각 스캔의 형태이며, 실제 작업을 수행하는 부분만 단순화하여 설명했습니다.
Will과 would: 모든 것의 시작
문서(Documentation)는 시스템이 불특정한 미래의 어느 시점에 어떻게 행동할지가 아니라, 현재 어떻게 행동하는지를 설명합니다. 따라서 "클라이언트는 실패한 요청을 재시도할 것입니다(the client will retry failed requests)"라는 문장은 시제 측면에서 작은 거짓말입니다. 클라이언트는 실패한 요청을 재시도합니다. 현재형, 직설법, 읽는 시점에 진실인 상태여야 합니다.
제가 계속 남기게 되었던 코멘트는 이것이었습니다: 현재의 동작은 현재 시제로 설명해야 하므로, "will retry"는 "retries"가 되어야 합니다. 매주 그랬습니다. 때로는 일주일에 두 번씩이나요.
스캔 방식은 이름만큼이나 단순합니다:
# will/would: 현재 시제로 작성되어야 할 산문 내의 미래 및 조건부 프레이밍
grep -nE '\b(will|would)\b' "$added_prose_lines"
오탐(False positives)은 실제로 존재하며, 저는 의도적으로 이를 그대로 두었습니다. 때로는 "will"이 올바를 때가 있기 때문입니다. 예를 들어, 지원 중단(deprecation) 공지("이 파라미터는 v3에서 제거될 것입니다(this parameter will be removed in v3)")는 미래에 대한 진정한 진술입니다. 그래서 이 스캔은 플래그(flag)를 표시할 뿐, 차단(block)하지는 않습니다. 스캔은 저에게 줄 번호 목록을 전달하고, 저는 그중 어떤 것이 시제 오류이고 어떤 것이 정당한 표현인지 결정하는 데 10초를 씁니다. 이 10초는 제가 실제로 사용하는 시간입니다. 왜냐하면 이 목록이 제 앞에 놓여 있기 때문에, 제가 그냥 건너뛰었을 세심한 읽기 과정을 대신하게 해주기 때문입니다.
수동태: 플래그는 표시되지만, 차단되지는 않음
이것은 제가 실행하는 스캔 중 가장 노이즈가 많고, 정확성 측면에서 가장 신뢰하지 않는 스캔입니다. 바로 그렇기 때문에 이 스캔은 절대 아무것도 차단하지 않습니다.
교체된 코멘트: 여기서 능동태 (active voice) 사용을 고려하여 독자가 누가 무엇을 하는지 알 수 있게 하세요. "The token is validated by the middleware"는 행위자를 문장 뒤로 숨깁니다. 반면 "The middleware validates the token"은 행위자를 앞으로 가져옵니다.
정규 표현식 (regex)으로 수동태를 탐지하는 것은 투박한 작업입니다. "be" 동사의 형태 뒤에 과거 분사 (past participle)가 오는 패턴을 찾아야 하는데, 영어에는 grep만으로는 완전히 인코딩할 수 없는 복잡한 규칙들이 있습니다:
# 수동태: 'be' 동사 형태 뒤에 과거 분사로 추정되는 단어가 오는 경우
grep -nEi '\b(is|are|was|were|been|being)\s+\w+(ed|en)\b' "$added_prose_lines"
이 방식은 실제 수동태를 잡아냅니다. 하지만 "embedded"가 아주 정직하게 제 역할을 수행하고 있는 "the data is embedded" 같은 경우도 잡아내며, 불규칙 과거 분사로 구성된 수동태는 놓치기도 합니다. 주말을 통째로 써서 이 규칙을 정교하게 다듬을 수도 있겠지만, 그러지는 않을 것입니다. 수동태는 정규 표현식이 정말로 좋은 판단과 나쁜 판단을 구분할 수 없는 유일한 차원이기 때문입니다. "The request is rate-limited"는 괜찮은 문장입니다. 스캔 시스템은 이를 알 방법이 없습니다. 따라서 시스템은 후보군을 노출할 뿐, 결정에는 관여하지 않습니다. 저는 리스트를 읽고, 그 자리에 있을 가치가 있는 것들만 남깁니다.
플레이스홀더 (Placeholders): TODO, TBD, 그리고 배포되어 버린 대괄호
이 규칙은 특정한 굴욕적인 사건 때문에 존재합니다. [TODO: confirm the actual rate limit]가 병합된 문서 PR (Pull Request)에 포함되었고, 한 스프린트 동안 공개 문서에 그대로 남아 있다가 제가 아닌 다른 개발자에게 발견된 적이 있습니다.
이 규칙이 교체한 코멘트는 문장조차 아니었습니다. 그것은 완성된 건물에서 비계 (scaffolding)를 발견했을 때 내뱉는 한숨과 같았습니다.
# 플레이스홀더: 산문 속에 남겨진 비계
grep -nE '(TODO|TBD|FIXME|XXX|lorem ipsum)' "$added_prose_lines"
흥미로운 튜닝 문제는 대괄호(brackets)였습니다. 길을 잃은 [fill this in] 같은 문구를 찾아내고 싶었지만, 마크다운 링크 문법(markdown link syntax)은 정당한 대괄호로 가득 차 있었고, CLI 문서([options], [--flag]) 또한 마찬가지였습니다. 첫 번째 버전은 페이지의 모든 링크를 잡아냈고, 저는 거의 이 아이디어 전체를 포기할 뻔했습니다. 해결책은 지루했습니다. 인라인 코드 스팬(inline code spans)과 링크 문법을 제거한 후, 산문(prose) 라인에 대해서만 대괄호 검사를 실행하는 것이었습니다. 이러한 스캔을 튜닝하는 작업의 대부분은 스캔이 어디를 보지 말아야 할지를 가르치는 과정입니다.
마케팅 용어들
문서(Documentation)는 마케팅 사이트에서 빌려온 어휘를 사용하곤 하며, 이를 다시 돌려주어야 합니다. "이 강력한(powerful) 엔드포인트는 단순히(simply) 사용자 객체를 반환합니다."라는 문장에서 '강력한(powerful)'은 아무런 역할을 하지 않습니다. '단순히(simply)'는 아무것도 하지 않는 것보다 더 나쁩니다. 왜냐하면 해당 내용을 어렵게 느끼는 독자에게, 원래는 쉽게 느껴졌어야 했다는 메시지를 전달하기 때문입니다.
제가 계속 남겼던 코멘트는 두 가지였습니다. "'powerful'은 마케팅 용어이지 문서 용어가 아닙니다. 삭제하세요." 그리고 "'simply'는 독자에게 그것이 간단하다고 가정하고 있습니다. 독자는 동의하지 않을 수도 있습니다." 입니다.
그래서 다음과 같은 단어 목록이 있습니다:
# 마케팅 침투 및 독자 탓을 하는 최소화 표현들
grep -nEiw '(powerful|robust|seamless|effortless|blazing|simply|just|easily|obviously)' "$added_prose_lines"
여기서 'just'와 'easily'가 골칫거리입니다. 왜냐하면 "전체 세트가 아닌 단지(just) Authorization 헤더만"과 같은 표현은 'just'의 정당한 사용이지만, 스캔 도구는 그 차이를 구분할 수 없기 때문입니다. 다른 곳과 마찬가지로 답변은 동일합니다. 도구가 플래그를 표시하면, 제가 검색 결과를 훑어보고, 독자의 어려움을 과소평가(minimize)하는 표현은 삭제하고, 정직하게 범위를 좁히는 표현은 유지합니다. 이 목록은 의도적으로 짧게 유지합니다. 금지어 목록이 너무 길면 노이즈가 너무 많이 발생하여 무시하기 시작하게 되고, 모두가 무시하는 게이트는 추가 단계만 더해진 체크박스 연극(checkbox theater)에 불과하기 때문입니다.
문장이 사실(fact)이어야 했을 때
이것은 다섯 가지 중 가장 미묘하며 제가 여전히 튜닝 중인 부분입니다. 이는 독자가 실제로 필요로 하는 사실을 대신하여 사용된, 확신이 없거나 조건부인 산문(hedged, conditional prose)을 잡아냅니다.
"헤더에 따라 캐시가 무효화될 수도 있고 그렇지 않을 수도 있습니다." 이 문장은 정보의 형태를 띠고 있지만, 내용은 어깨를 으쓱하는 식의 무책임함을 담고 있습니다. 독자는 예/아니오 질문(내 요청이 캐시를 무효화하는가?)을 가지고 있는데, 이 문장은 "아마도"라고 답합니다. 어딘가에는 사실이 존재합니다. Cache-Control: no-store를 설정하면 캐시가 무효화되며, 다른 값들은 캐시를 그대로 유지합니다. 이 완곡한 표현(hedge)은 제가 그냥 명시할 수 있었던 조건을 숨기고 있었습니다.
이것이 대체한 코멘트: 이 문장은 불확실하게 읽히지만, 그 밑에는 결정론적인(deterministic) 규칙이 있으므로 완곡하게 표현하는 대신 조건을 명시하세요.
# 명시된 조건이 독자에게 더 도움이 될 수 있는 완곡한 산문(hedged prose) 탐색
grep -nEi '(may or may not|can either|might also|sometimes|in some cases|it depends)' "$added_prose_lines"
이 부분에 대한 솔직한 고백을 하자면, 이 스캔은 수동태(passive voice) 다음으로 다섯 가지 중 가장 높은 오탐률(false-positive rate)을 보입니다. 왜냐하면 때로는 불확실성이 실제로 존재하기 때문입니다. 어떤 동작은 깔끔한 규칙으로 환원할 수 없는 런타임 상태(runtime state)에 진정으로 의존합니다. 따라서 이 스캔은 grep 명령 자체는 사소할지라도, 실제 적용 시에는 이들 중 가장 덜 기계적인 방식입니다. 이것은 주로 하나의 프롬프트(prompt)로서 작동합니다. "may or may not"이라고 썼는데, 혹시 당신이 피하고 있는 사실이 여기 있는 것 아닌가요? 그리고 종종 사실이 존재합니다.
스캔이 볼 수 없는 것
정규 표현식(regex)은 의미가 아니라 형태를 포착합니다. 수동태 스캔은 "토큰이 미들웨어에 의해 검증됩니다"라고 표시하며, 대부분의 경우 그것은 옳습니다. 하지만 이 스캔은 "미들웨어가 토큰을 검증합니다"가 더 나은 문장이라는 것을 알려줄 수는 없습니다. 왜냐하면 문장이 무엇을 위한 것인지 모르기 때문입니다. 스캔은 문장이 수동태의 형태를 띠고 있다는 것만 압니다. 그것이 스캔이 아는 전부입니다.
따라서 이 스캔들 중 그 어느 것도 무언가를 결정하지 않습니다. 스캔은 후보군을 드러낼 뿐입니다. 결정은 제가 합니다. 이것이 사람들이 "자동화된 리뷰(automated review)"라는 말을 들을 때 기계가 리뷰를 수행하는 모습을 상상하며 놓치게 되는 부분입니다.
분업(division of labor)에 대해 정확히 짚고 넘어갈 가치가 있습니다. 왜냐하면 그것이 바로 이 시스템이 작동하는 근본적인 이유이기 때문입니다. 스캔(scan)은 제 판단을 대체하기 위해 존재하는 것이 아닙니다. 그것은 제 판단을 저 자신의 일관성 부족으로부터 보호하기 위해 존재합니다. 저는 특정 문장에서 수동태(passive construction)가 적절하게 사용되었는지 결정하는 데는 능숙합니다. 하지만 목요일 오후 4시에 그것을 찾아내야 한다는 사실을 기억하는 데는 서툽니다. 스캔은 결코 잊지 않고 찾아내는 역할을 수행합니다. 저는 스캔이 전혀 할 수 없는 결정하는 역할을 수행합니다. 우리는 각자 실제로 신뢰할 수 있는 역할을 할당받은 것입니다.
아직 스캔이 없는 것들
이 방식은 제가 이름을 붙일 수 있는 실패 모드(failure modes)들을 처리합니다. 즉, 패턴으로 인식할 수 있을 만큼 충분히 자주 입력하여 정규 표현식(regex)으로 축약할 수 있는 것들 말입니다. 그 아래에는 또 다른 범주가 있습니다. 아직 제가 지치지 않은 코멘트들인데, 왜냐하면 아직 단 한 번만 처리해 보았기 때문입니다. 그런 것들은 스캔이 없습니다. 대신 로그(log)가 있습니다.
그것이 다음 단계입니다. 격차 로그(gap log), 그리고 하나의 PR(Pull Request)에서 배운 교훈이 다음 PR에서 실행할 스캔이 될 수 있을 만큼 충분히 오래 살아남게 만드는 진정으로 짜증 나는 문제입니다. 저는 이를 완전히 해결하지는 못했습니다. 하지만 실행 가능한 형태는 갖추어져 있으며, 마찰(friction)이 여전히 어디에 있는지 정확히 보여드리겠습니다. 왜냐하면 그것이 매끄럽다고 가장하는 것은 저 스스로가 만든 '마케팅 용어 목록'에 저를 올리는 일이 될 것이기 때문입니다.
만약 여러분이 이런 스캔을 실행한다면, 저는 무엇보다 먼저 여러분의 오탐(false-positive) 목록을 보고 싶습니다. 진짜 지식은 바로 그곳에 살고 있습니다. 저의 경우, 스캔을 작성하는 것보다 튜닝(tuning)하는 데 훨씬 더 오랜 시간이 걸렸으며, 이 튜닝이야말로 아무도 기사에 쓰지 않는 부분입니다.
저는 AI 보조 문서화 워크플로우(AI-assisted documentation workflows), 개발자 경험(developer experience), 그리고 기술 작가(technical writer)의 진화하는 역할에 대해 글을 씁니다. 이 내용 중 공감되는 부분이 있다면, LinkedIn에서 연결해 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기