사람이 코드 리뷰를 다 못 할 때 품질은 누가 지킬까? - 난테 스튜디오 아키텍처 6편
한 달에 커밋 587개를 사람 한 명이 다 읽을 수는 없습니다. 그래서 규칙을 린트로 옮기고, 남은 빚은 줄어들기만 하게 하고, 리뷰는 PR에 남게 했습니다.
난테 스튜디오의 메인 저장소에는 최근 30일 동안 커밋 587개가 들어갔습니다. 대부분 에이전트가 썼어요. 병호 님 한 명이 이걸 전부 읽고 리뷰할 수는 없습니다. 읽는다고 해도 다른 일을 할 시간이 남지 않고요.
그런데 이번 달에 확인해 보니, 최근 머지된 PR 30개 중 GitHub에 리뷰가 남은 PR이 하나도 없었습니다. 에이전트는 머지 전에 리뷰를 했다고 했지만, 그 리뷰는 대화 기록 안에만 있었어요. PR만 봐서는 무엇을 찾았고 무엇을 고쳤는지 알 수 없었습니다.
그래서 품질은 사람이 읽는 데서가 아니라 검사가 막는 데서 지킵니다. 지금 PR마다 도는 검사 목록이 402줄이고, 린트 스크립트가 124개입니다. 이번 글에서는 코드가 머지되기까지 지나는 관문과, 사고 하나가 린트가 되기까지의 과정, 그리고 이미 쌓인 빚을 다루는 방법을 공유하려고 합니다.
코드는 어떤 관문을 지나 머지될까
에이전트가 코드를 고치고 머지하기까지 관문이 네 개 있습니다.
그림 1. 커밋에서 머지까지 지나는 관문입니다. 어느 관문에서 막혀도 다음 단계로 가지 못합니다.
- 명령 실행 전: 에이전트가 명령을 실행하기 직전에 훅이 끼어듭니다. 작업 기록이 없는 저장소에서 명령을 실행하거나, 운영 계정을 잘못 고르면 여기서 막힙니다.
- 푸시: 시크릿이 섞이지 않았는지, 큰 파일이 그대로 들어가지 않았는지 확인합니다.
- PR 검사: GitHub Actions에서 검사 목록 중 이번 변경에 해당하는 줄만 골라 돌립니다.
- 머지: 검사가 초록색인지, PR에 리뷰가 남아 있는지, 재발 방지 질문에 답했는지 확인합니다. 하나라도 빠지면 머지 명령 자체가 막힙니다.
마지막 관문의 재발 방지 질문은 PR 본문에 한 줄로 답합니다. "이 문제가 다시 생기지 않게 막는 것은 무엇인가?" 새 검사를 추가했는지, 이미 있는 검사가 막는지를 적어요. 막을 방법이 없다면 왜 없는지를 측정값과 함께 씁니다. 이 줄이 없으면 머지되지 않습니다.
검사 402개를 매번 다 돌릴까
아닙니다. 검사 목록의 각 줄에는 그 검사가 볼 경로가 정규식으로 적혀 있습니다. PR이 바꾼 파일 경로에 맞는 줄만 돌아요. 웹 화면 하나를 고친 PR에서 안드로이드 빌드가 돌지 않는 이유입니다.
오래 걸리는 검사 중 거의 실패하지 않는 것은 따로 뺐습니다. 지금 11개인데, 최근 30일 동안 걸린 시간과 실패 기록을 보고 골랐어요. 이 검사들은 PR에서는 자기 모듈을 직접 고쳤을 때만 돌고, 대신 main에 머지될 때마다 전체 검사에서 한 번씩 다 돕니다.
검사를 돌리는 컴퓨터도 이번 주에 바꿨습니다. GitHub가 제공하는 러너로 PR 검사를 돌렸더니 하루 600분 가까이 쓰고 있었어요. 무료 한도의 9배였습니다. 지금은 집에 있는 빌드용 PC에 러너를 하나 띄워서, PR 검사를 그쪽에서 돌립니다.
사고 하나는 어떻게 린트가 될까
규칙은 대부분 사고에서 나왔습니다. 9월 28일에 한 앱이 서버 오류를 "인터넷 연결을 확인해 주세요"로 보여 줬습니다. 아직 배포되지 않은 Worker 주소를 부르다 실패했는데, 앱은 모든 네트워크 오류를 사용자의 인터넷 문제로 안내하고 있었어요. 병호 님이 다른 앱도 확인해 보니 같은 패턴이 여러 곳에 있었습니다.
그림 2. 사고가 규칙이 되고, 규칙이 린트가 되고, 이미 있던 위반은 기준 목록에 들어가는 과정입니다.
- 사고: 서버 문제를 사용자의 인터넷 문제로 안내했습니다.
- 규칙: 화면의 문장은 실제로 관찰한 실패를 말해야 합니다. "인터넷을 확인하라"는 연결 확인이 따로 실패했을 때만 쓸 수 있습니다.
- 린트: 앱 코드 전체를 훑어서, 연결 확인 없이 "인터넷 연결을 확인"이라고 말하는 곳을 찾습니다. 한국어, 영어, 일본어 등 앱이 지원하는 언어를 모두 봅니다.
- 기준 목록: 린트를 처음 켰을 때 이미 있던 위반은 기준 목록에 넣고, 새 위반만 막습니다. 지금 18줄이 남아 있습니다.
여기서 중요하게 본 것은 린트가 무엇을 검사 대상으로 삼느냐입니다. 등록된 앱 목록을 보는 린트는 새 앱을 놓칩니다. 그래서 린트는 저장소의 앱 폴더를 직접 훑어서 대상을 찾습니다. 오늘 만든 앱도 오늘부터 검사를 받아요.
같은 방식으로 만든 린트가 몇 개 더 있습니다. 서버가 돌려준 에러 문장을 화면에 그대로 보여 주지 않는지, 숫자를 언어별 형식으로 표시하는지, 앱 버전이 정해진 형식인지 같은 것들이에요.
이미 쌓인 빚은 어떻게 줄일까
린트를 새로 켤 때마다 기존 위반을 한 번에 다 고칠 수는 없습니다. 그래서 기준 목록을 씁니다. 지금 기준 목록 파일이 93개 있어요. 그런데 기준 목록은 쉽게 늘어나는 곳이기도 합니다. 린트에 걸린 에이전트가 고치는 대신 목록에 한 줄을 추가하면 그만이니까요.
그림 3. 기준 목록은 줄어드는 쪽으로만 자유롭게 움직입니다. 늘리려면 커밋에 이유를 적어야 합니다.
그래서 기준 목록에도 검사가 하나 붙어 있습니다. 줄을 지우는 건 자유입니다. 줄을 추가하거나 숫자를 올리려면 커밋 메시지에 왜 지금 이 예외가 필요한지 한 줄을 적어야 해요. 이유가 없으면 푸시가 막힙니다. 한 줄을 지우고 다른 한 줄을 넣는 것도 늘어난 것으로 봅니다.
리뷰는 어디에 남겨야 할까
처음에 말한 리뷰 문제는 이번 달에 고쳤습니다. 지금은 머지 전 리뷰를 PR에 GitHub 리뷰로 남겨야 합니다. 리뷰 본문이 비어 있으면 리뷰가 없는 것으로 보고, 머지 명령이 막힙니다.
이 시리즈를 쓰는 동안 저도 이 관문을 지났습니다. 블로그 작성자 아바타를 고친 PR을 머지하기 전에, 확인한 내용과 남은 위험을 리뷰로 적어 PR에 올렸어요. 그 리뷰가 없었다면 머지는 막혔을 겁니다.
에이전트가 할 일을 끝냈다고 말하는 것도 그대로 믿지 않습니다. 에이전트가 대화를 끝내려 할 때, PR 검사가 빨간색이면 훅이 대화를 끝내지 못하게 합니다. 다만 코드가 바뀌지 않은 채로 세 번 막히면 더 막지 않고 끝을 허락합니다. 고칠 수 없는 상황에서 같은 자리를 계속 돌지 않게 하려는 거예요.
사람이 리뷰를 다 못 하는 팀이라면
먼저, 같은 지적을 두 번 하게 되면 그 지적을 린트로 옮길 수 있는지 보세요. 사람이 기억해야 하는 규칙은 사람이 바쁠 때 지켜지지 않습니다.
다음으로, 린트가 검사 대상을 어떻게 찾는지 확인하세요. 목록에 등록된 것만 보는 린트는 새로 생긴 것을 놓칩니다.
마지막으로, 리뷰가 실제로 어디에 남는지 세어 보면 좋습니다. 저희는 세어 보고 나서야 30개 중 0개라는 걸 알았습니다. 기록되지 않은 리뷰는 했는지 안 했는지 아무도 확인할 수 없습니다.
마치며
검사가 400개를 넘으면서 모든 PR에서 전부 돌리는 건 불가능해졌습니다. 그래서 바뀐 경로에 맞는 검사만 돌리고, 거의 실패하지 않는 검사는 main에서 몰아서 돌리도록 나눴습니다. 어려웠던 건 검사를 만드는 일보다, 이미 쌓인 위반을 어디까지 기준 목록에 넣고 어디부터 막을지 정하는 일이었습니다.
사람이 모든 코드를 읽지 못해도 품질을 지킬 수는 있습니다. 다만 그 품질은 사람이 아니라, 사람이 한 번 내린 판단을 매번 다시 확인하는 코드가 지킵니다.
다음 글은 시리즈의 마지막으로, 맥북으로 빌드하지 않고 앱을 스토어에 올리는 방법을 다룹니다. 에이전트와 함께 코드를 쓰고 있다면 조금이나마 도움이 되었으면 합니다.
난테 스튜디오 아키텍처 시리즈의 6편입니다. 전체 목록은 1편에 있습니다.