새벽 2시 조금 전에 PR 하나가 올라왔다. 로그인이 풀리는 장애를 막는 수정이었고, 배포는 그날 오후 3시로 잡혀 있었다. 아침까지 리뷰가 열 건 넘게 달렸고 승인이 여섯 번 찍혔다. 그런데 설계를 정한 건 그중 어느 리뷰도 아니었다. 아침 10시에 누군가 서버에 요청을 한 번 보내 본 결과였다.

리뷰는 많았는데, 방향을 바꾼 건 측정 한 번이었다. 결정적인 질문은 AI 리뷰가 이미 던져 놓았지만 아무도 확인하지 않았다. 그 질문을 똑같이 던져 놓고 승인 버튼을 누른 사람 중에 나도 있었다.

이 글은 그걸 어떻게 알게 됐는지, 그리고 내 리뷰 에이전트를 지금 어떻게 고치고 있는지 적은 기록이다. 뒤에서는 이번 일에서 본 AI 리뷰의 문제 네 가지와, 그 각각에 붙인 규칙을 적었다.

상황 — 새벽부터 아침까지

장애를 막는 수정이라 시간이 없었다. 작성자는 처음에 “끊어진 세션을 서버에서 되살린다”는 방향으로 구현했다. 새벽 3시 반, 그 방향을 “되살리지 말고 정리한다”로 바꿨다. 아침에는 같이 넣었던 로그아웃 쪽 변경을 통째로 되돌렸다. 10시 조금 전 그가 남긴 말은 “아직 핑퐁 치고 있습니다”였다.

그 사이 리뷰가 쌓였다. 자동 리뷰 봇, 동료들이 돌린 AI 리뷰, 사람 리뷰, 작성자가 직접 돌린 셀프 리뷰 봇, 그리고 내가 Codex 로 돌린 리뷰까지. 각자 맞는 말을 했다. 승인도 여섯 번 붙었다.

아침 10시에 방향이 정해졌다. 설계는 “그 상태에서는 서버가 세션을 되살려 줄 수 없다”는 전제 위에서 흔들리고 있었는데, 요청을 한 번 보내 보니 서버는 되살려 줬다. 그 전제는 서버의 제약이 아니라 우리 코드에 있던 방어 로직이었다. 새벽에 걷어낸 첫 구현이 맞았다. PR 은 그 방향으로 돌아갔고 30분 만에 정리됐다.

발견 — 지적 열두 건은 어디로 갔나

나중에 리뷰를 하나씩 따라가 봤다. 작성자가 지적을 반영할 때마다 답글에 커밋 해시를 적어 둔 덕에, 각 지적이 어떻게 끝났는지 거의 기계적으로 셀 수 있었다.

인라인 지적은 열두 건이었다. 네 건은 코드에 반영됐지만, 그 코드가 나중에 통째로 되돌려지면서 같이 사라졌다. 다섯 건은 지적한 코드 자체가 다음 커밋에서 빠졌다. 두 건은 반박됐고 한 건은 보류됐다. 최종 코드에 남은 지적은 하나뿐이었다. 되돌려졌던 네 건 중 하나가, 아침에 원래 방향으로 돌아가면서 함께 복원된 것이다.

승인 여섯 번 중 네 번은 바로 옆에 열린 질문이 있었다. “승인, 고쳐야 할 부분 없음” 리뷰와 “확인 필요” 네 건이 거의 같은 시각에 따로 올라온 경우가 있었다. 테스트를 여러 개 돌려 검증했다는 승인도 있었는데, 그 테스트는 서버와 쿠키를 전부 흉내 낸 것이었다.

그리고 내 리뷰가 있었다. 아침 10시 조금 전에 돌린 내 Codex 리뷰는 세 가지를 지적했는데, 셋 다 35분 전에 다른 AI 리뷰가 거의 같은 말로 남긴 것이었다. 그중 하나는 맞는 질문이었다. “앱이 세션의 일부만 되살려 넣는 경로가 있다면, 이 수정은 앱 사용자를 매번 로그아웃시키지 않나.” 나는 1분 뒤 승인했다. 그 질문은 “운영에서 확인할 사항으로 남긴다”는 한 줄로 바꿔 놓고. 작성자도 같은 질문에 “맞습니다, 리스크를 PR 본문에 적었습니다”라고 답했다. 질문은 맞았고 답도 성실했는데, 아무도 확인하지 않았다.

누가 게을렀다는 얘기가 아니다. 새벽에 가장 많이 일한 사람은 작성자였고, AI 리뷰들도 각자 맞는 말을 했다. 그런데 맞는 말들이 서로 겹쳤고, 리뷰의 절반 가까이가 아침이면 사라질 코드를 보고 있었고, 열린 질문은 승인 아래에 묻혔다.

개선 — 내 리뷰 에이전트부터 고치고 있다

남의 리뷰를 바꾸기는 어렵다. 내가 돌리는 리뷰 에이전트는 바로 바꿀 수 있다. 원래 프롬프트도 꽤 잘 짜여 있었다. 심각도 기준이 있고, 회차마다 같은 코드를 다시 들추지 말라는 규칙이 있고, “후속 티켓에서 처리”는 해결이 아니라는 규칙도 있다. 그런 규칙이 있는데도 새어 나간 경로가 있었고, 그 경로만 골라 막았다.

세 가지를 만들었다. 리뷰 에이전트에 얹는 규칙 문서, 리뷰를 시키기 전에 붙여 넣는 프롬프트, 그리고 머지 뒤 지적별로 반영됐는지, 걷어냈는지, 보류됐는지를 세는 스크립트다. 위의 숫자도 그 스크립트로 셌다. 규칙은 아래 네 가지 문제에 하나씩 짝지었다.

문제 1 — 리뷰들이 서로를 읽지 않는다

내가 쓰던 리뷰 에이전트도 diff 와 아직 해결되지 않은 스레드만 입력으로 받았다. 해결된 스레드나 다른 리뷰의 본문은 보지 않으니, 이미 같은 말이 나왔는지 모른다. 그래서 같은 지적이 서너 번 올라오고, 작성자는 같은 내용을 서너 번 읽고 서너 번 답한다. 내 아침 리뷰가 앞선 리뷰를 되풀이한 것도 그래서였다.

규칙: 쓰기 전에 이미 나온 지적을 다 읽는다. 다른 리뷰어의 리뷰, 해결된 스레드, PR 대화까지 입력에 넣는다. 같은 주제가 있으면 새로 쓰지 않고, 새 근거가 있을 때만 그 스레드에 답한다. 동의만 할 거면 요약에 한 줄이면 된다. 작성자 쪽에서도 같은 주제는 한 스레드에서 판단하고 나머지에는 링크로 답한다.

문제 2 — 근거의 무게가 보이지 않는다

코드를 읽고 추론한 지적, 테스트로 돌려 본 지적, 실제 환경에서 재현한 지적이 같은 모양으로 올라온다. “테스트로 검증했다”는 문장은 무게가 있어 보이지만, 그 테스트가 서버와 쿠키를 흉내 냈다면 서버와 쿠키에 대해서는 아무것도 확인하지 않은 것이다.

규칙: 지적마다 근거 등급을 단다. 실제 환경에서 재현했으면 📏, 테스트로 돌렸으면 🧪, 코드와 타입만으로 증명되면 📖, 그런 경로나 상태가 있다고 가정만 했으면 ❓. 흉내 낸 경계에 대해서는 테스트도 📖 로 친다. 치명 등급은 실행 이상이어야 하고, ❓ 는 질문으로만 남긴다. 이 등급은 PR 작성자 쪽에서 리뷰에 대응할 때 쓰던 이름과 맞췄다. 리뷰하는 쪽과 받는 쪽이 같은 말을 쓰게 하려고.

문제 3 — 질문은 던지는데 답은 하지 않는다

이번에 가장 선명하게 보인 문제다. AI 리뷰는 “이 전제가 맞는지 확인해 보시라”고 묻는 데 꽤 능숙하다. 결정적인 질문도 AI 리뷰가 먼저 했다. 그런데 그 질문에 답하려면 코드 밖으로 나가야 한다. 서버에 요청을 보내 보고, 앱이 실제로 무엇을 넣는지 보고, 브라우저에서 쿠키를 지워 봐야 한다. 리뷰 에이전트는 거기서 멈추고, 멈춘 자리를 “운영 확인”, “리스크로 남김” 같은 보류 문구와 승인이 메운다.

규칙: 전제 문장을 먼저 뽑고, 보류에는 주인을 붙인다. PR 본문과 주석에서 “~없다”, “~불가”, “정상이면 생기지 않는다” 같은 문장을 찾아 목록으로 만든다. 설계를 정하는 외부 전제는 리뷰의 첫 항목이고, 지금 확인할 수 있으면 실제로 돌려서 결과를 붙인다. 확인할 수 없으면 누가, 어디서, 무엇으로 확인할지 적는다. “운영 확인”이나 “본문에 리스크로 적음”을 쓰는 순간 그건 열린 질문이고, 열린 질문이 있으면 승인하지 않는다. 작성자 쪽에도 같은 규칙을 걸었다. 내 설계 전제가 질문받으면, 말로 반박하지 말고 한 번 실측한다.

문제 4 — 결과를 재지 않는다

어떤 리뷰가 실제로 코드를 바꿨는지 아무도 모른다. 그러니 같은 체크리스트가 수십 번 찍히고, 승인 숫자만 쌓인다. 「AI 가 리뷰하고, 나는 AI 로 승인했다」에서 목적 없는 테스트를 셌을 때도 같은 이야기였다. 결과를 재고 다음 실행을 고치는 고리가 없으면 실행 횟수만 늘어난다.

규칙: 머지 뒤에 지적별 결과를 센다. 반영됐는지, 반영됐다가 걷어냈는지, 코드가 사라졌는지, 보류됐는지, 반박됐는지. 리뷰어나 봇별로 실제로 코드를 바꾼 지적의 비율을 본다. PR 몇 건 치를 모으면 어떤 규칙이 효과가 있었는지 보이고, 그걸로 다시 규칙을 고친다.

규칙을 하나 더 붙였다. 잘한 판단은 이름을 붙여 짚는다. 이번에 작성자는 마지막 수정에서 내 제안보다 세 군데를 더 꼼꼼하게 반영했다. 승인 코멘트에 그 세 가지를 하나씩 적고, 내 제안 자체에 빠져 있던 부분은 내 몫이라고 밝혔다. 밤을 새워 일한 사람에게 필요한 건 “LGTM” 이 아니라, 무엇을 잘 판단했는지 누군가 정확히 봤다는 신호였다.

불편한 이야기

이 글을 정리하던 늦은 오전, 같은 PR 에 일곱 번째 승인이 붙었다. 결함은 없다며 코멘트 여덟 건을 남긴 승인이었다. 점검표에는 항목마다 이모지가 찍혀 있었고, 코드 위치도 정확히 인용돼 있었다. 다른 리뷰와 겹치는 세 건은 “살펴봤지만 문제가 아니었던 것”으로 접어 두기까지 했다. 그날 본 리뷰 중 가장 꼼꼼해 보였다.

그런데 여덟 건을 주제로 묶으니 다섯 개였다. 같은 줄에 같은 지적이 세 번 달려 있었다. 다른 리뷰와의 중복은 걸렀는데, 자기 리뷰 안의 중복은 거르지 못했다. 결함이 없다며 승인했지만 확인을 부탁하는 질문 두 건이 열려 있었고, 점검표 곳곳에는 빨간 표시가 있었다. 가장 쓸모 있는 지적은 특정 경로에서 서버 재발급이 페이지마다 반복된다는 것이었는데, 그 지적은 중복 세 건과 확인되지 않은 공격 시나리오 하나 사이에 끼어 있었다. 배포까지 세 시간 남짓 남은 때였다.

더 불편한 건 세 번 반복된 그 지적의 내용이었다. 새로 넣은 경고 로그가 요청마다 에러 수집 도구로 간다는 지적이었는데, 그 로그는 내가 아침에 “배포 후 빈도를 보려면 로그를 남기자”고 제안해서 들어간 것이었다. 리뷰의 제안도 결국 코드가 된다. 나는 그 제안이 어디로 흘러갈지 확인하지 않았다.

돌아보면 이번 PR 의 리뷰는 거의 전부 AI 를 거쳤다. 자동 리뷰 봇, 동료들의 AI 리뷰, 작성자의 셀프 리뷰 봇, 내 Codex, 그리고 마지막 리뷰까지. 내가 PR 에 남긴 제안과 이 글도 AI 와 함께 썼다. 리뷰 하나를 만드는 비용은 거의 0이 됐는데, 그걸 읽고 가려내는 비용은 그대로 작성자 한 사람에게 갔다. 새벽 4시 가까이 일한 그 사람에게.

이번이 처음도 아니었다. 한 달쯤 전, 다른 동료의 PR 하나에 리뷰 봇이 네 시간 동안 여섯 번 리뷰를 달았다. 커밋을 올릴 때마다 새 지적이 붙었고, 합치면 지적 스물한 건과 확인 요청 열한 건이었다. 봇이 걸어 둔 변경 요청 상태는 작성자가 직접 풀 수 없는 종류여서, 결국 다른 사람이 대신 승인해 막힌 걸 풀었다. 작성자는 우는 이모지와 함께 남은 지적은 다음 PR 로 넘기겠다고 했다. 그 동료는 그 무렵 2주 사이에 봇에게 “코멘트 남겨 두었습니다, 재리뷰 부탁드려요”를 열두 번 보냈고, 이전 지적과 같은 말이 다시 달린 적도 있었다. 작성자가 코드를 고치는 사람이 아니라 봇을 돌리는 사람이 되어 있었다.

중복되고 가치가 낮은 지적이 이렇게 밀려오면, 개발자에게 남는 일은 고치는 게 아니라 고르는 일이 된다. 어떤 지적이 진짜고 어떤 게 반복인지, 무엇을 지금 반영하고 무엇을 넘길지, 무엇에 반박할지. 지적 하나는 몇 초면 읽히지만, 서른 개를 앞에 두고 무엇을 어떻게 반영할지 정하는 데는 그 몇십 배가 든다. 마감이 걸린 날이면 그 판단은 “일단 다 반영하자”나 “일단 승인하자”로 무너지기 쉽다. 한 달 전 그 PR 도, 이번 PR 도 결국 그렇게 끝났다.

그래서 이건 누구 한 사람의 리뷰 습관 문제가 아니다. 우리 모두 비슷한 도구로 비슷하게 리뷰하고 있고, 각자의 리뷰는 혼자 보면 꼼꼼하다. 모아 놓으면 같은 말이 겹치고, 판정이 엇갈리고, 중요한 지적이 묻힌다. 형식이 엄격해 보일수록 판정은 오히려 흐려졌다. 체크리스트와 이모지가 많을수록 “그래서 막는 건가, 통과인가”를 작성자가 해석해야 했다.

일곱 번째 승인을 보고 규칙을 두 줄 더 붙였다. 내 리뷰 안에서도 지적을 주제로 묶어서, 같은 줄에 같은 말을 두 번 쓰지 않는다. 그리고 리뷰어의 제안에도 지적과 같은 무게를 둔다. 로그 한 줄, 호출 한 번을 더하자는 제안이면 그게 요청마다 어디로 가는지까지 적는다.

그래서, AI 리뷰는 이렇게 해야 한다

코드 리뷰를 다룬 2013년 마이크로소프트 연구에서 개발자들은 리뷰에서 결함 찾기를 가장 먼저 기대했지만, 실제로 리뷰가 남긴 건 그보다 코드 개선과 지식 공유 쪽이 많았다. 가장 큰 어려움으로 꼽힌 건 변경을 이해하는 일이었다. 구글의 코드 리뷰 가이드도 리뷰어가 볼 것은 완벽함이 아니라 “이 변경이 코드베이스를 분명히 낫게 만드는가”라고 말한다.

AI 리뷰는 변경을 읽는 일을 거의 공짜로 만들었다. 그런데 이번에 막힌 건 읽기가 아니었다. 코드 밖의 사실 하나를 확인하는 일이었다. 리뷰가 많아질수록 그 확인을 누군가 해 줄 거라고 서로 믿기 쉬워진다. 승인이 일곱 번 찍힌 PR 에서도 그 확인을 한 리뷰는 없었다.

그래서 지금 내 결론은 이렇다. AI 리뷰는 질문을 만드는 데 쓴다. 질문에 답하는 일과 승인은, 근거 등급과 확인할 사람을 붙여서 한다. 질문을 던지는 건 에이전트에게 맡겨도 된다. 이미 나온 질문을 다시 던지지 않게 하는 것, 답이 나올 때까지 승인을 미루는 것, 그리고 어떤 질문이 실제로 코드를 바꿨는지 세는 것은 아직 사람이 설계해 줘야 한다.

마치며

겉멋 AI에서 가장 비싼 건 토큰이 아니라 동료의 주의력이라고 썼다. 이번에는 그 주의력이 어디로 새는지가 보였다. 같은 지적을 여러 번 읽는 데로, 사라질 코드를 리뷰하는 데로, 확인되지 않은 질문을 승인으로 덮는 데로. 그중 하나는 내 리뷰였다.

이 규칙들이 효과가 있는지는 아직 모른다. 오늘 만들었고, 아직 한 건도 재지 않았다. PR 몇 건 뒤에 같은 스크립트로 다시 세어 보고, 무엇이 줄었고 무엇이 그대로인지 적을 생각이다. 규칙 몇 줄로 다 막히지는 않겠지만, 적어도 다음 리뷰에서는 내 에이전트가 그 세 곳으로 새는지 셀 수는 있다.