리뷰 자동화 이야기를 하면 사람들은 보통 속도를 떠올린다. 리뷰가 밀려서, 며칠씩 기다려야 해서.

우리는 그 앞 단계에 있었다. 리뷰가 없었다.

각자 프로젝트를 하나씩 끌고 가는 구조에서 남의 레포를 열어 diff를 읽는 일은 언제나 내 일보다 뒤에 놓인다. 악의가 있어서가 아니다. 그저 우선순위에서 매번 진다.

혼자 담당하는 레포는 애초에 리뷰어 후보가 없었고, 사내 GitLab에는 Runner가 없어 CI가 대신 잡아주는 것도 없었다. 결과는 둘 중 하나였다. 리뷰 없이 머지되거나, 리뷰를 기다리다 잊히거나.

없는 것을 만든다

사람을 더 뽑을 수 없다면 선택지는 좁아진다. 그래서 봇을 만들었다.

사내 GitLab에 계정을 하나 만들고, 열린 MR을 찾아 로컬에서 diff를 읽고 코멘트를 다는 파이프라인을 붙였다. 여기까지는 누구나 그릴 수 있는 그림이다.

실제로 굴려보니 어려운 지점은 다른 데 있었다. 무엇을 리뷰할 것인가보다, 이 봇을 어디까지 믿을 것인가가 문제였다.

상태를 어디에 둘 것인가

봇을 회사 맥과 집 맥에서 각각 돌리면 같은 MR을 두 번 리뷰하는 사고가 난다.

처음엔 로컬 파일에 리뷰 기록을 남겨 막으려 했다. 곧 접었다. 머신이 둘이 되는 순간 로컬 기록은 자기 세계의 진실만 알고 있다.

그래서 코멘트 안에 보이지 않는 표식을 심었다. HTML 주석이라 화면에는 드러나지 않지만 API로는 조회된다. 리뷰하기 전에 그 표식을 찾아보면 된다.

기록의 정본이 GitLab 자체가 되니 어느 머신에서 돌려도 안전해졌다. 로컬 파일은 캐시로 내려갔다.

분산된 곳에서 같은 일을 하는 여럿이 있을 때, 진실을 어디에 두느냐가 대부분의 문제를 결정한다는 걸 이때 다시 배웠다.

침묵할 줄 아는 것

리뷰가 끝난 뒤 커밋이 더 쌓인 MR을 처음부터 다시 리뷰하면, 이미 한 말을 또 한다. 받는 사람에게 그건 리뷰가 아니라 소음이다.

그래서 표식에 담긴 이전 시점부터의 변경만 본다. 그리고 이전에 남긴 지적이 반영됐는지도 그때 확인한다.

한 가지 더 정한 게 있다. 확신이 서지 않는 지적은 게시하지 않는다. 나에게만 보고하고 끝낸다.

자동화된 코멘트는 사람의 코멘트보다 무게가 가볍다. 틀린 지적이 몇 번 쌓이면 사람들은 봇의 말을 읽지 않게 되고, 그다음부터는 맞는 지적도 지나친다. 말할 수 있는 것과 말해야 하는 것은 다르다.

봇이 할 수 없는 일

봇의 토큰은 코멘트만 달 수 있다. 머지도 승인도 라벨도 못 하고, git에는 아무것도 쓰지 못한다. 정해진 그룹 밖은 아예 보지 않는다.

자동화를 설계할 때 기능 목록만큼 오래 붙들고 있었던 게 금지 목록이었다. 자동화가 한 번 사고를 내면 그 도구는 다시 쓰이지 않는다. 편의는 조금씩 쌓이지만 신뢰는 한 번에 무너진다.

리뷰 기준도 봇 안에 넣지 않고 문서로 뺐다. 기준을 바꾸고 싶을 때 코드를 고치는 대신 문서를 고친다. 그러면 그건 나만 할 수 있는 일이 아니게 된다.

요청에 담은 것

봇 자체는 AI가 만들었다. 내가 정한 건 무엇을 할 수 있게 하느냐가 아니라 무엇을 못 하게 하느냐였고, 그래서 규칙 문서의 마지막 절이 금지 목록이다.

- front 그룹 밖은 어떤 경우에도 금지.
- 봇 토큰으로 코멘트 외 쓰기 API(머지·승인·라벨) 호출 금지.
- 같은 SHA 마커가 있으면 재게시 금지. 판단이 서지 않는 지적은 게시하지 말고 사용자 보고에만 남긴다.

세 줄 모두 기능이 아니라 제약이다. 마지막 줄이 특히 그렇다. 침묵을 기본값으로 두지 않으면 봇은 확신 없는 말을 계속 하게 되고, 그러면 사람이 봇의 말을 읽지 않게 된다.

중복 방지도 같은 문서 안에서 정본을 못 박았다.

- 중복 확인 (게시 직전에도 재확인): 해당 MR의 notes에서 `<!-- mr-review:sha=<head_sha> -->` 마커 검색.
  있으면 이미 리뷰됨 → 스킵.
  `state.json`은 로컬 캐시일 뿐 — GitLab 마커가 항상 우선이다.

마커에 커밋 SHA를 넣은 것이 이 설계의 전부다. MR 단위로 표시하면 커밋이 더 쌓여도 다시 리뷰하지 않고, 리뷰 시점 단위로 표시하면 어디부터 다시 봐야 하는지가 마커 자체에 적힌다.

로컬 캐시를 남겨 두되 정본이 아니라고 문장으로 못 박은 것도 의도였다. 캐시는 지워도 되는 것이어야 한다. 지웠을 때 사고가 나면 그건 캐시가 아니라 데이터베이스다.

무엇이 남았나

이제 모든 MR이 최소한 한 번은 읽힌다. 혼자 담당하는 레포도 예외가 아니다.

기계적인 지적을 봇이 먼저 깔아주니, 사람이 하는 리뷰는 자연스럽게 설계와 맥락 쪽으로 올라갔다. 왜 이 화면이 이렇게 생겨야 하는지, 이 결정이 다음 분기에 어떤 빚이 되는지 같은 것들.

가장 크게 달라진 건 사실 기술이 아니라 마음이었다. 부탁할 사람이 없어서 그냥 머지하던 시절의 찝찝함이 사라졌다.

그리고 남은 질문이 하나 있다. 우리가 없앤 것은 리뷰의 부재였을까, 아니면 리뷰를 부탁하기 어려웠던 관계의 문제였을까.

기계가 할 수 있는 리뷰를 기계에 넘겼더니, 사람의 리뷰가 필요한 자리가 오히려 또렷해졌다.