대학 때 은사님께 들은 이야기가 하나 있다. 다익스트라의 수업을 들은 적이 있는데, 그는 강의 유인물을 워드프로세서 없이 손으로 써서 나눠 줬다고 한다. 이유를 물으니 워드프로세서 덕에 정크 페이퍼가 늘어서 자기는 쓰지 않는다고 했다는 것이다. 공식 석상에서 한 말인지, 은사님께만 한 말인지는 모른다. 다만 그가 평생 손으로 쓴 원고를 번호 붙여 남긴 건 기록에 있다. EWD 아카이브의 번호는 1300 을 넘는다.
그 이야기를 오늘 오후에 떠올렸다.
오후 1시 26분
PR 을 하나 올렸다. 로그인 세션이 반쪽만 남았을 때 서버에서 되살리는 수정이었다. 2분 뒤 첫 승인이 붙었고, 8분 사이에 지적 여덟 건과 승인 하나가 더 붙었다. 리뷰어 셋은 전부 자동 리뷰였다.
같은 채널에서는 동료 한 명이 테스트 PR 을 올리고 있었다. 4분 간격으로 열한 건. 각각 파일 하나, 테스트만 백 줄에서 이백 줄. 운영 코드는 한 줄도 바뀌지 않았다. 각 PR 에는 올라간 지 1분에서 4분 사이에 리뷰가 다섯에서 일곱 건씩 붙었다. 43분 동안 PR 열한 건, 리뷰 예순세 건. 사람이 손으로 쓴 리뷰는 없었다. 그 시점까지 머지된 PR 도 없었다.
오전에 리뷰 하나를 만드는 비용이 거의 0이 됐다고 썼다. 오후에 보니 리뷰만이 아니었다. 테스트를 쓰는 비용도, 리뷰에 답하는 비용도 거의 0이 됐다. 셋 다 만드는 쪽은 공짜가 됐고, 읽고 가려내는 쪽만 사람에게 남았다. 이 글은 그 세 가지가 하루에 겹친 날의 기록이고, 그중 내가 손댈 수 있었던 한 곳에 무엇을 붙였는지에 대한 이야기다.
공짜가 된 세 가지
테스트. 지난주에 한 달 반 치 테스트 PR 을 세어 봤다. 운영 코드는 거의 건드리지 않고 비어 있던 테스트를 채우는 PR 들이었다. 결함은 테스트를 쓰는 순간에 네 건 나왔고, 머지된 뒤 그 테스트가 잡은 에러는 0건이었다. 오늘 열한 건이 어떤 결함을 찾았는지는 아직 모른다. 다만 같은 종류의 PR 이 한 달 반 동안 남긴 숫자는 그랬다.
리뷰. 오전 글에 적었다. PR 하나에 리뷰 열두 건, 승인 일곱 번. 방향을 바꾼 건 서버에 요청 한 번 보내 본 측정이었고, 최종 코드에 남은 지적은 하나였다. 리뷰어는 자동 리뷰 봇, 동료들이 돌린 AI 리뷰, 작성자의 셀프 리뷰 봇, 내 Codex 였다.
리뷰 답글. 오늘 오후의 이야기다. 지적 여덟 건에 답하는 데 한 시간이 들었다. 그 한 시간이 이 글의 가운데 토막이다.
세 가지의 공통점은 “만드는 비용이 0” 이라는 것만이 아니다. 셋 다 만든 사람과 치우는 사람이 다르다. 테스트 PR 을 올리는 사람과 그걸 리뷰하는 사람, 리뷰를 다는 봇과 그걸 읽는 작성자, 답글을 다는 작성자와 그걸 다시 읽는 리뷰어. 비용은 늘 다음 사람에게 간다. 다익스트라의 정크 페이퍼도 그랬을 것이다. 쓰는 사람은 편해졌고, 읽는 사람의 책상에 종이가 쌓였다.
PR 도 정크 페이퍼가 된다
리뷰가 문제라고만 쓰면 반만 쓴 것이다. 리뷰는 PR 이 있어야 돈다. PR 을 만드는 비용이 0이 되면 리뷰 비용은 그 뒤에 자동으로 따라온다.
오늘 테스트 PR 열한 건을 올린 동료의 지난 6주를 세어 봤다. PR 85건. 그중 54건이 운영 코드를 한 줄도 바꾸지 않는 테스트 전용 PR 이었고, 42건이 머지됐다. 더해진 테스트 코드는 1만 3천 줄이 넘는다. 하루에 열 건 이상 올라온 날이 두 번 있었고, 오늘이 그중 하나다. 열한 건은 전부 같은 모양이었다. 파일 하나, 테스트 백 줄에서 이백 줄, 그리고 2천 자 남짓한 본문. 본문의 구조는 열한 건이 똑같았다. 변경 요약, 검증한 것, 영향 범위. 사람이 열한 번 쓴 글이 아니라는 건 읽으면 안다.
이 동료를 탓하려는 게 아니다. 지난주 글에서 고백했듯 그 PR 들을 “회귀 방지 가치가 있다”며 승인한 사람 중에 나도 있다. 그리고 테스트를 쓰는 것 자체는 좋은 일이다. 문제는 PR 이라는 단위가 무엇인지에 있다. PR 은 “이걸 봐 달라”는 요청이다. 다른 사람의 시간을 쓰겠다는 선언이다. 오늘 PR 한 건이 올라갈 때마다 자동 리뷰가 다섯에서 일곱 건 붙었고, 채널에 알림이 하나 떴고, 누군가 그 PR 을 한 번은 열어 봐야 했다. 43분 동안 열한 번. 같은 채널에서 같은 시간, 장애를 막는 수정 PR 이 리뷰를 기다리고 있었다.
가치 쪽을 보면 더 분명해진다. 지난주에 센 한 달 반 치 테스트 PR 은 머지된 뒤 단 한 번도 CI 를 빨갛게 만들지 않았다. 결함은 테스트를 쓰는 순간에 네 건 나왔는데, 그건 PR 을 올려서 나온 게 아니라 테스트를 써 보다가 나온 것이다. 그 네 건이 발견된 뒤에 PR 로 올라올 이유는 있었다. 그 밖의 PR 들이 각각 리뷰 봇 다섯 바퀴와 사람의 눈을 쓸 이유가 있었는지는, 머지 뒤 숫자로는 보이지 않는다.
다익스트라의 정크 페이퍼는 글이었다. 오늘 우리 책상에 쌓인 건 PR 이다. 한 사람이 43분 동안 리뷰 요청 열한 건을 만들어 낼 수 있는 날, PR 을 올리는 자리에도 거르는 질문이 필요하다는 게 보였다. 이 테스트는 실패해 본 적이 있는가. 무엇을 지키는가. 지금 리뷰어의 시간을 쓸 만큼 급한가. 세 질문 중 하나에도 답이 없으면 그건 아직 PR 이 아니라 작업 중인 브랜치다.
여덟 건을 한 시간 안에
지적 여덟 건을 하나씩 열어 “이 지적이 딛고 선 전제가 지금 맞는가”만 확인했다. 확인은 대부분 명령 한 번이었다.
| 지적 | 확인한 것 | 걸린 시간 | 결과 |
|---|---|---|---|
| 정적 페이지라 공유 캐시가 다른 사용자의 세션 쿠키를 저장할 수 있다 | 운영 응답 헤더를 네 경로에서 직접 봤다. 전부 캐시를 타지 않는 설정이었다 | 1분 | 해당 없음 |
| 토큰이 회전하면 동시 요청에서 정상 세션까지 지워진다 (두 리뷰어) | 어제 QA 중 같은 토큰으로 되돌려도 세션이 유지되는 걸 이미 재 봤다. 회전하지 않는다 | 0분, 재사용 | 전제 불성립 |
| 복구된 첫 요청의 서버 렌더는 새 쿠키를 못 본다 | 서버에서 로그인 쿠키를 읽는 곳은 한 페이지뿐이고, 수정 전에도 같은 결과였다 | 5분 | 맞지만 회귀 아님, 기록 |
| 인증 서버 장애 때 요청마다 2초씩 늦어진다 | 맞다. 조건은 반쪽 세션과 서버 장애가 겹칠 때 | 1분 | 맞음, 기록 |
| 실패 원인을 로그에 남기지 않는다 | 같은 경로의 클라이언트 코드가 이 실패를 일부러 로깅하지 않는다. 노이즈 때문에 몇 달 전 제거한 기록이 있다 | 2분 | 선례로 닫음 |
| 레거시 패키지를 새로 가져왔다 | 메인 브랜치에서 그 패키지를 이미 쓰는 파일이 네 앱에 열여섯 개 있었다 | 1분 | 전제 불성립 |
| 주석이 엉뚱한 코드를 설명하게 됐다 | 맞다 | 2분 | 고침 |
고친 건 하나. 근거를 대고 닫은 게 다섯, 맞지만 이번 범위가 아니라 기록으로 남긴 게 둘. 리뷰 세 개의 심각도 표기는 전부 달랐고([확인필요], 🔍, P2), 판단에 쓰지 않았다. 열어 보면 P2 하나가 가장 약했고 [개선] 하나가 유일하게 고칠 것이었다.
여덟 건을 다 반영했으면 하루가 들었을 것이다. 다 무시했으면 2분이면 됐을 것이다. 하나씩 확인하니 한 시간이었다.
그런데 진짜 구멍은 리뷰 밖에 있었다
그 한 시간 사이에 다른 동료가 QA 시나리오 하나를 밟았다. 네트워크를 끊고 로그아웃 버튼을 누른다. “정상적으로 로그아웃되었습니다”가 뜬다. 네트워크를 켜고 마이페이지를 새로고침한다. 화면이 제대로 안 나온다. 거기서 로그인 버튼이나 장바구니를 누르면, 로그인 절차 없이 그대로 로그인된다.
이유는 단순했다. 로그아웃은 서버 호출 두 번으로 이뤄지는데, 둘 다 실패해도 클라이언트는 자기가 지울 수 있는 쿠키만 지우고 성공 안내를 띄운다. 클라이언트가 못 지우는 httpOnly 토큰이 살아 있으니 다음 페이지에서 세션이 되살아난다. 내 PR 은 “되살리는 범위”를 넓히는 수정이었다. 그 전에 “되살리면 안 되는 세션”을 막는 코드가 필요했다.
그 코드는 있었다. 새벽 2시에 들어갔다가 아침 9시 47분에 되돌려졌다. 되돌린 이유는 커밋에 적혀 있었다. “로그인 페이지가 그 세션을 정리하므로.” 그런데 48분 뒤 다음 커밋에서 로그인 페이지는 그 세션을 정리하지 않고 되살리는 쪽으로 바뀌었다. 되돌림의 근거가 사라졌는데, 아무도 보지 못했다. 리뷰는 diff 를 보고, 전제는 커밋 사이에 있다.
그때까지 내 PR 에 붙은 자동 리뷰 열다섯 건 중 이 경로를 짚은 건 없었다. 반쪽 세션이 “앱이 토큰만 넣어 주는 경로”에서 생긴다는 PR 본문의 설명을 그대로 전제로 삼고 있었다. 그 세션이 “로그아웃에 실패했을 때”도 생긴다는 건 코드를 읽어서는 안 나온다. 네트워크를 끊어 봐야 나온다.
되돌려진 커밋을 다시 살려 PR 에 넣었다. 테스트 835건이 통과했다. 그걸 넣기 전까지 내 PR 은 승인 두 개를 달고 있었다.
새로운 이야기는 아니다. 마이크로소프트가 2015년에 낸 연구의 제목부터가 “코드 리뷰는 버그를 찾지 않는다”였다. 리뷰 코멘트 대부분은 결함이 아니라 유지보수성에 관한 것이었고, 결함은 다른 곳에서 나왔다. 그때까지 붙은 자동 리뷰 열다섯 건이 그 분포를 그대로 보여 줬다. 리뷰가 많은 PR 이 더 많이 검증된 PR 은 아니었다.
걸러내는 자리에 절차를 둔다
다익스트라는 만드는 쪽을 느리게 했다. 손으로 쓰면 한 장을 채우기 전에 한 번 더 생각하게 된다. 우리는 그럴 수 없다. 도구는 이미 모두의 손에 있고, 리뷰 봇은 PR 이 올라오면 묻지 않고 돈다. 만드는 쪽을 느리게 할 수 없으면, 걸러내는 자리에 절차를 둬야 한다.
오늘 하루에 걸러내는 자리가 네 군데 있었다. PR 을 올리는 사람, 그 PR 을 받는 리뷰어, 리뷰를 다는 봇, 리뷰를 받는 작성자. 첫 번째는 위에 적은 세 질문이고, 아직 절차라기보다 질문이다. 두 번째는 지난주 글에서 승인 답글에 “이 테스트가 깨지면, 누가 보나요?”를 직접 쓰기로 했다. 세 번째는 오전 글에서 고쳤다. 이미 나온 지적을 읽고, 근거 등급을 붙이고, 질문에 답이 나올 때까지 승인을 미루는 규칙이었다. 네 번째가 오늘 오후 몫이다. 내가 바로 손댈 수 있는 자리가 거기였다.
구글의 코드 리뷰 가이드에는 리뷰어용 문서 옆에 작성자용 문서가 따로 있다. 개인적으로 받아들이지 말 것, 동의하면 코드를 고치고 아니면 왜인지 설명할 것, 의견이 갈리면 같이 보자고 할 것. 다 맞는 말인데, 지적 하나를 앞에 두고 어떻게 대할지에 대한 이야기다. 여덟 건이 8분 안에 올 때 먼저 필요한 건 그 앞 단계다. 어떤 지적에 그 과정을 쓸지 고르는 절차.
내가 쓰는 리뷰 대응 에이전트에 넣은 건 다섯 줄이다.
“확인한 것” 칸을 먼저 채운다. 비어 있으면 값을 매기지 않는다. 위 표의 두 번째 열이다. 이 칸이 비면 판단은 심각도 표기나 리뷰어의 말투를 따라가게 된다. 채우고 나면 대부분 결론이 저절로 나온다.
값은 넷 중 하나다. 고친다, 기록한다, 근거로 닫는다, 묶는다. “고친다”는 전제가 확인됐고 이번 변경이 만든 문제고 비용이 작을 때다. “기록한다”는 맞는 말이지만 변경 전에도 같았을 때다. “근거로 닫는다”는 전제가 측정이나 선례로 무너질 때다. “묶는다”는 다른 리뷰어와 같은 축일 때로, 한 스레드에서만 답하고 나머지에는 링크를 건다.
확인은 싸야 한다. 오늘 쓴 건 다섯 가지였다. 운영 응답 헤더를 보는 것, 이미 재 둔 측정을 다시 쓰는 것, 메인 브랜치에서 기존 사용처를 세는 것, 같은 판단을 하는 기존 코드를 찾는 것, 변경 전에도 같았는지 보는 것. 각각 수 초에서 1분이다. 이보다 비싼 확인이 필요한 지적은 “확인되면 처리하겠습니다”로 열어 둔다.
심각도, 확신 점수, “병합 전에” 같은 문구는 값에 쓰지 않는다. 리뷰어마다 체계가 다르고, 같은 체계 안에서도 조용히 어긋난다. 오늘 Critical risk 와 Confidence 4/5 가 같은 리뷰에 붙어 있었다.
지적이 넷 이상이면 표 한 장으로 보고한다. 건별 서술은 내부 판정용이고, 사람에게는 위 표와 “고침 1 · 기록 2 · 근거로 닫음 5” 한 줄이면 된다.
“고친다”가 0건이나 1건이어도 정상이다. 답글로 끝나는 지적이 많다는 건 리뷰가 쓸모없었다는 뜻이 아니라, 확인이 싸게 끝났다는 뜻이다.
숫자 하나 더
고친 것과 되살린 커밋을 푸시했다. 3분 뒤 자동 리뷰가 일곱 건 더 왔다. 테스트에서 전역 객체를 덮어쓰고 복원하지 않았다는 정확한 지적도 있었고, 문서 표에 새 함수를 안 적었다는 지적도 있었다. 그리고 다시 한 바퀴다.
비용을 만드는 건 리뷰어 수가 아니라 라운드 수였다. 구글이 리뷰 속도를 말할 때 재는 것도 왕복 한 번에 드는 시간이다. 커밋을 올릴 때마다 리뷰 봇들이 한 바퀴 돌고, 작성자는 매 바퀴 표를 다시 채운다. 오전 글에서 한 동료의 PR 에 봇이 네 시간 동안 여섯 번 리뷰를 단 이야기를 썼다. 그때는 리뷰어 쪽 문제로 봤다. 오후에 내 PR 에서 바퀴를 돌아 보니, 받는 쪽에 절차가 없으면 바퀴마다 “일단 다 반영”과 “일단 승인” 사이에서 흔들리게 된다는 게 보였다.
세 번째 바퀴부터는 달랐다. 되살린 로그아웃 코드에 지적 넷이 붙었는데, 셋이 진짜였다. 새벽에 넣은 예외 하나가 그 뒤 커밋으로 전제를 잃었다는 것, 실패 분기가 로그아웃을 자동으로 부르는 화면에서 멈추지 않고 반복된다는 것, 실패해도 멈췄다는 걸 호출한 화면에 알려 주지 않아 버튼이 잠긴 채 남는다는 것. 셋 다 코드를 읽어서 나올 수 있는 지적이었고, 셋 다 고쳤다. 테스트 PR 을 열한 건 올린 그 동료가 돌린 자동 리뷰가 그중 둘을 찾았다. 그러니 이 글은 자동 리뷰가 쓸모없다는 글이 아니다. 열아홉 건 중 코드의 동작을 바꾼 결함은 셋이었고, 그 셋을 찾으려면 열아홉 건을 전부 열어 전제를 확인해야 했다는 글이다. 절차가 없으면 그 셋은 “다 무시”에 묻히고, 나머지 열여섯은 “다 반영”으로 하루를 먹는다.
정크 페이퍼
다시 은사님 이야기로 돌아간다. 다익스트라가 정말 그렇게 말했는지는 확인할 길이 없다. 그래도 그 말이 오늘 하루에 맞아떨어진 이유는 분명하다. 그가 경계한 건 워드프로세서 자체가 아니었을 것이다. 만드는 비용이 떨어질 때 읽는 사람의 비용은 그대로라는 것.
오늘 그 비용이 어디로 갔는지 세어 보면 이렇다. 테스트 PR 열한 건을 누군가 읽어야 한다. 그 PR 들에 붙은 자동 리뷰 예순세 건을 작성자가 읽어야 한다. 내 PR 에 붙은 지적 열아홉 건을 내가 읽어야 한다. 그리고 그 모든 것 사이에서, 네트워크를 끊어 보는 사람이 한 명 있어야 한다. 오늘 결함을 찾은 건 그 한 명이었다.
걸러내는 절차는 그 한 명의 시간을 지키려고 만든다. 지적마다 확인한 것 한 칸, 값 하나. 리뷰어 쪽에는 이미 나온 지적을 다시 쓰지 않는 규칙. 테스트 PR 에는 “이 테스트가 깨지면, 누가 보나요?”라는 질문. 어느 것도 만드는 쪽을 느리게 하지는 않는다. 다만 종이가 책상에 쌓이기 전에 한 번 거른다.
아직 모르는 것
이 절차에도 구멍이 있다. 가장 큰 건 “확인한 것”이 재사용된 측정일 때다. 오늘 토큰 회전 지적 두 건을 어제 측정으로 닫았다. 어제 맞았던 게 오늘도 맞다는 보장은 없다. 측정에 날짜를 붙이고, 그 날짜가 오래되면 다시 재는 규칙이 더 필요하다.
네 자리 중 둘에는 아직 절차가 없다. PR 을 올리는 쪽에는 질문 세 개를 적었을 뿐이고, 그 질문을 올리는 사람이 스스로 하게 만들 방법은 모른다. 리뷰 봇 쪽에는 “이 PR 에 봇 다섯이 각자 돌 필요가 있었는가”라는 질문이 남아 있다. 오늘 열한 건에 붙은 예순세 건 중 몇 건이 없어도 됐는지 세어 보지 않았다. 단서는 하나 있다. 이 글을 쓰는 사이에 열한 건 중 둘이 머지됐는데, 둘 다 커밋은 하나였다. 각각 여섯 건씩 붙은 리뷰는 코드를 한 줄도 바꾸지 않았다. 테스트만 바뀐 PR 에는 한 봇만 돌게 하는 게 맞을 수도 있다. 세어 보면 알 것이다.
다음에 잴 것은 둘이다. 라운드마다 “고친다”의 비율이 어떻게 바뀌는지. 그리고 머지된 변경에서 발견된 결함 중 리뷰가 짚은 것과 리뷰 밖에서 나온 것의 비율. 두 번째 숫자가 이 절차의 한계를 알려줄 것이다.
마치며
오전에 “질문은 에이전트가 만들고, 답과 승인은 근거 등급과 확인할 사람을 붙여서 한다”고 썼다. 오후에 하나를 더 적는다. 답하는 쪽에도 절차가 있어야 한다. 그리고 그 절차는 리뷰에만 필요한 게 아니다. 만드는 비용이 0이 된 모든 곳, PR 과 테스트와 리뷰와 답글이 쌓이는 모든 책상에 필요하다.
다시 다익스트라다. 은사님께 들은 워드프로세서 이야기는 확인할 길이 없지만, 그가 1975년에 적어 둔 문장은 아카이브에 그대로 있다. “우리가 쓰는 도구는 우리의 사고 습관에, 그래서 사고 능력에, 깊은 (그리고 교활한!) 영향을 미친다.” 교활하다는 말에 괄호와 느낌표까지 쳐 놓았다. 오늘 하루가 그 괄호 안에 있었다. 도구는 PR 을 4분에 하나씩 만들게 했고, 리뷰를 1분 안에 달게 했고, 답글을 한 번에 여덟 개 쓰게 했다. 누구도 나쁜 뜻은 없었다. 습관이 먼저 바뀌었고, 무엇을 걸러야 하는지 생각하는 능력은 그 뒤를 따라오지 못했다. 그 글의 제목이 “아픈 진실을 어떻게 말할 것인가”였다는 것도 오늘에 와서 보니 우연 같지 않다.
그가 남긴 더 유명한 한 줄도 같은 서랍에 있다. “프로그램 테스트는 버그가 있다는 것은 보여 줄 수 있어도, 없다는 것은 결코 보여 주지 못한다.” 테스트만 그런 게 아니었다. 리뷰 열아홉 건은 지적이 있다는 걸 보여 줬고 그중 셋은 결함도 보여 줬지만, 결함이 없다는 건 보여 주지 못했다. 승인 두 개도 마찬가지였다. 결함이 있다는 걸 보여 준 건 네트워크를 끊어 본 한 사람이었다.
겉멋 AI에서 가장 비싼 건 토큰이 아니라 동료의 주의력이라고 썼다. 오늘 그 주의력이 지켜야 했던 건 리뷰 열아홉 건도, 테스트 PR 열한 건도 아니었다. 그 한 사람의 몇 분이었다. 다익스트라가 끝까지 손으로 쓴 이유도 아마 그 몇 분이었을 것이다. 한 장을 채우기 전에 멈추는 몇 분.
그래서 다음부터는 PR 을 올리기 전에, 리뷰를 달기 전에, 답글을 쓰기 전에 한 번 멈추고 묻겠다. “이거, 손으로 썼어도 올렸을까?”
읽을 거리
- EWD498 — How do we tell truths that might hurt? — “도구는 사고 습관에 교활한 영향을 미친다”가 들어 있는 1975년 메모. 한 장짜리다
- EWD249 — Notes on Structured Programming — “테스트는 버그의 존재만 보여 준다”의 출처
- Code Reviews Do Not Find Bugs — 리뷰 코멘트의 대부분이 결함이 아니었다는 2015년 연구
- Google eng-practices — Handling Reviewer Comments — 리뷰를 받는 쪽을 위한 몇 안 되는 공개 가이드