Posts

코드 리뷰는 죄수의 딜레마일까

목차9
한쪽만 보이는 거울로 나뉜 두 취조실에서 두 개발자가 각자 같은 문서를 화면으로 보고 있는 그림
두 사람은 지금 서로의 PR 을 보고 있다. 서로가 얼마나 꼼꼼한지는 모른다

금요일 오후 네 시, 리뷰 요청 알림이 하나 뜬다. 파일 32개, 변경 1,200줄짜리 PR 이다. 꼼꼼히 보려면 적어도 한 시간은 걸릴 것 같은데, 내 앞에도 끝내야 할 작업이 쌓여 있다. 이럴 때 머릿속에서 아주 작은 목소리가 들린다. "작성자가 알아서 잘 테스트했겠지."

그렇게 누른 승인 버튼에는 보통 네 글자가 따라붙는다. LGTM. (Looks Good To Me 의 약자지만, 가끔은 Let's Go To Merge 에 더 가깝다)

코드 리뷰가 형식적으로 변해가는 걸 두고 흔히 개인의 성실함을 탓한다. 물론 성실함도 중요하다. 그런데 사실 성실한 사람들이 모인 팀에서도 리뷰는 조금씩 느슨해진다. 이건 개인의 문제가 아니라 구조의 문제일 가능성이 크다.

그래서 이번 포스팅에서는 코드 리뷰를 게임 이론의 가장 유명한 게임인 죄수의 딜레마로 한번 계산해보려고 한다. 리뷰가 왜 구조적으로 느슨해지는지, 그럼에도 어떤 팀은 왜 협력을 유지하는지, 그리고 그 조건을 수식으로 쓰면 어떻게 되는지를 차례대로 이야기해보자.

죄수의 딜레마

죄수의 딜레마는 1950년 RAND 연구소의 메릴 플러드(Merrill Flood)와 멜빈 드레셔(Melvin Dresher)가 고안한 게임에서 시작됐다. 이 게임에 두 죄수의 이야기를 입혀서 지금의 모습으로 만든 사람은 수학자 앨버트 터커(Albert Tucker)로 알려져 있다. 자세한 역사는 스탠퍼드 철학 백과사전에 잘 정리되어 있는데, 이름을 붙인 사람이 정확히 누구인지는 지금도 약간의 논쟁이 있다고 한다.

원래 이야기는 이렇다. 공범인 두 사람이 따로 취조실에 갇혀 있고, 서로 말을 맞출 수 없다. 둘 다 입을 다물면 둘 다 가벼운 형을 받고, 한 명만 자백하면 자백한 사람은 풀려나고 다른 한 명은 무거운 형을 받는다. 둘 다 자백하면 둘 다 꽤 무거운 형을 받는다.

이 이야기를 코드 리뷰로 옮겨보자. 리뷰어 A 와 B 는 서로의 PR 을 리뷰한다. 각자 고를 수 있는 선택지는 꼼꼼히 리뷰하는 것과 대충 승인하는 것 두 가지다. 꼼꼼한 리뷰는 시간이 들지만 상대의 코드 품질을 올려주고, 대충 승인은 내 시간을 아끼지만 상대의 코드에 버그가 섞여 들어가게 둔다.

숫자는 설명을 위해 정한 값이다. 각자 합리적으로 고르면 오른쪽 아래 칸에 도착한다

위 표의 숫자는 "아낀 시간과 받은 리뷰의 가치를 합친 만족도" 정도로 생각하면 된다. 내가 대충 승인하고 상대는 내 PR 을 꼼꼼히 봐주면, 나는 시간도 아끼고 좋은 리뷰도 받으니 5점이다. 반대로 나만 꼼꼼히 보고 상대는 대충 승인하면, 나는 시간만 쓰고 받은 건 없으니 0점이 된다.

게임 이론에서는 이 네 가지 보상에 이름을 붙인다. 유혹(Temptation)의 TT, 보상(Reward)의 RR, 처벌(Punishment)의 PP, 그리고 속은 사람의 몫(Sucker)인 SS 다. 어떤 게임이 죄수의 딜레마가 되려면 이 값들이 아래 조건을 만족해야 한다.

T>R>P>S,2R>T+ST > R > P > S, \qquad 2R > T + S

첫 번째 조건은 "배신의 유혹이 협력의 보상보다 크고, 서로 배신하는 것이 혼자 속는 것보다는 낫다"는 뜻이다. 두 번째 조건은 "둘이 번갈아 가며 속고 속이는 것보다 꾸준히 협력하는 쪽이 낫다"는 뜻이다. 위 표의 값 T=5T = 5, R=3R = 3, P=1P = 1, S=0S = 0 은 두 조건을 모두 만족한다.

합리적인 선택이 모두를 손해 보게 만든다

이제 A 의 입장에서 생각해보자. B 가 꼼꼼히 리뷰한다면, A 는 꼼꼼히 할 때 3점, 대충 할 때 5점을 받는다. B 가 대충 승인한다면, A 는 꼼꼼히 할 때 0점, 대충 할 때 1점을 받는다. 즉, B 가 무엇을 하든 A 는 대충 승인하는 쪽이 이득이다.

B 도 똑같이 계산하니, 결국 두 사람 모두 대충 승인을 고르고 각자 1점을 받는다. 둘 다 꼼꼼히 했다면 3점씩 받을 수 있었는데 말이다. 각자 가장 합리적인 선택을 했는데, 결과는 두 사람 모두에게 나쁜 슬픈 엔딩이다. 이게 죄수의 딜레마가 유명해진 이유다.

일상에서 이 구조를 가장 생생하게 겪는 곳은 아마 대학의 조별 과제일 것이다. 다른 조원이 열심히 하면 나는 조금 덜 해도 같은 학점을 받는다. 다른 조원이 대충 하면 나 혼자 열심히 해봐야 손해만 본다. 그래서 모두가 조금씩 덜 하게 되고, 마감 전날 밤에 다 같이 후회하는 익숙한 장면이 완성된다. 코드 리뷰는 학점 대신 코드 품질을 걸고 매주 반복되는 조별 과제인 셈이다.

…

리뷰가 느슨해지는 건 누가 게을러서가 아니라, 한 번만 놓고 보면 느슨한 리뷰가 합리적이기 때문이다.

재미있는 건, 이 게임을 처음 만든 플러드와 드레셔도 실제 사람들이 이론대로 움직이지 않는다는 걸 일찍 확인했다는 점이다. 두 사람은 1950년 초에 동료 두 명에게 같은 보상 구조의 게임을 100번 연달아 두게 했는데, 참가자들은 이론의 예측보다 훨씬 자주 협력했다고 한다. 한 번만 두면 배신이 합리적인 게임인데, 여러 번 두니 사람들이 다른 답을 찾아낸 것이다. 필자는 이 실험이 코드 리뷰를 이해하는 데 가장 중요한 힌트라고 생각한다. 그 힌트가 무엇인지는 다음 섹션에서 이어서 이야기해보자.

한 번이 아니라 매일 하는 게임

그런데 여기서 이상한 점이 하나 있다. 위 계산대로라면 세상의 모든 팀에서 리뷰는 형식적이어야 한다. 하지만 실제로는 리뷰 문화가 꽤 건강하게 유지되는 팀도 많다. 그 팀들은 계산을 틀린 걸까?

정치학자 로버트 액설로드(Robert Axelrod)는 1980년 무렵 이 질문에 답하기 위해 재미있는 대회를 열었다. 게임 이론가들에게 죄수의 딜레마를 반복해서 두는 전략을 프로그램으로 제출받아, 서로 200번씩 대결시킨 것이다. 결과는 『협력의 진화』(The Evolution of Cooperation, 1984)라는 책으로 정리됐다.

우승한 전략은 놀라울 만큼 단순했다. 심리학자 아나톨 라포포트(Anatol Rapoport)가 낸 팃포탯(Tit for Tat)이라는 전략인데, 첫 판에는 협력하고 그 다음부터는 상대가 직전 판에 한 행동을 그대로 따라 하는 것이 전부다. 액설로드가 더 많은 참가자를 모아 두 번째 대회를 열었을 때도 이 단순한 전략이 다시 우승했다.

B 는 한 번 받은 만큼만 돌려주고 바로 원래대로 돌아온다. 뒤끝이 딱 한 라운드인 셈이다

액설로드는 팃포탯이 이긴 이유로 몇 가지 성질을 꼽았다. 먼저 배신하지 않는 착한 전략이고, 배신당하면 바로 갚아주며, 상대가 다시 협력하면 금방 용서하고, 행동이 단순해서 상대가 쉽게 읽을 수 있다는 것이다.

코드 리뷰로 바꿔 읽으면 꽤 익숙한 모습이다. 먼저 꼼꼼히 리뷰해주고, 상대가 내 PR 을 대충 넘기면 다음에는 나도 조금 덜 보고, 상대가 다시 성의를 보이면 나도 바로 원래대로 돌아간다. 우리가 "리뷰 잘해주는 동료"라고 부르는 사람들이 대체로 이렇게 움직인다.

오해 하나가 만드는 메아리

다만 팃포탯에도 약점이 있다. 상대의 행동을 잘못 읽는 경우다. 상대는 꼼꼼히 봤는데 내가 그걸 대충 본 것으로 오해하면, 나는 다음 라운드에 대충 승인으로 갚는다. 그러면 상대는 내 행동을 보고 또 대충으로 갚고, 두 사람은 아무도 원하지 않은 보복의 메아리 속에 갇힌다.

코드 리뷰에서 이런 오해는 생각보다 흔하다. "이 부분은 왜 이렇게 했나요?"라는 질문이 작성자에게는 비난으로 읽히고, 짧은 승인이 리뷰어의 무성의로 읽힌다. 텍스트로만 오가는 리뷰는 표정이 없어서, 같은 문장도 읽는 사람의 기분에 따라 전혀 다른 신호가 된다.

이 문제를 다룬 후속 연구들은 가끔 일부러 상대의 배신을 눈감아주는 너그러운 팃포탯이 잡음이 많은 환경에서 더 잘 버틴다는 결과를 보여줬다. 리뷰 문화로 옮기면, 상대의 짧은 코멘트를 일단 선의로 읽어주는 습관이 메아리를 끊는 셈이다. 코멘트 앞에 nit:, question:, suggestion: 같은 꼬리표를 붙여 의도를 드러내는 컨벤셔널 코멘트 같은 관례도, 결국 상대가 내 신호를 잘못 읽을 확률을 줄이려는 장치다.

미래의 그림자

반복 게임에서 협력이 유지되는 이유를 액설로드는 미래의 그림자(shadow of the future)라고 불렀다. 오늘 배신해서 얻는 이득보다, 내일 이후로 계속될 협력의 가치가 더 크면 배신할 이유가 없다는 것이다.

이 직관은 수식으로 꽤 깔끔하게 쓸 수 있다. 다음 라운드의 보상을 지금 가치로 얼마나 쳐주는지를 할인율 δ\delta 라고 하자. δ\delta 가 1에 가까우면 미래를 현재만큼 중요하게 여기는 것이고, 0에 가까우면 오늘만 신경 쓰는 것이다. 상대가 한 번이라도 배신하면 영원히 협력을 끊는 전략을 상대로 할 때, 계속 협력하는 것이 배신보다 나으려면 아래 조건이 필요하다.

R1−δ≥T+δP1−δ⟺δ≥T−RT−P\frac{R}{1-\delta} \ge T + \frac{\delta P}{1-\delta} \quad\Longleftrightarrow\quad \delta \ge \frac{T-R}{T-P}

왼쪽 식이 무섭게 생겼지만 하는 말은 단순하다. 왼쪽 항 R1−δ\frac{R}{1-\delta} 는 매 라운드 RR 을 영원히 받을 때의 총가치이고, 오른쪽은 이번에 한 번 TT 를 챙긴 뒤 그 다음부터는 계속 PP 만 받을 때의 총가치다. 이걸 정리하면 오른쪽의 깔끔한 부등식이 나온다.

코드
// 협력을 유지할 수 있는 최소 할인율
function cooperationThreshold(T: number, R: number, P: number) {
  return (T - R) / (T - P);
}

cooperationThreshold(5, 3, 1); // 0.5
cooperationThreshold(9, 3, 1); // 0.75, 대충 승인의 유혹이 클수록 경계가 올라감

우리 예시의 값을 넣으면 경계는 0.5다. 즉, 다음 리뷰 라운드를 지금의 절반 이상 가치로 여긴다면 꼼꼼한 리뷰가 합리적인 선택이 된다. 그리고 대충 승인했을 때 아끼는 시간, 즉 유혹 TT 가 커질수록 이 경계는 점점 올라간다. 1,200줄짜리 PR 이 꼼꼼한 리뷰를 받기 어려운 이유가 여기서도 보인다.

점의 위치는 감으로 찍은 값이다. 다만 마감 전날 밤이 왼쪽에 있다는 데에는 다들 동의할 것 같다

이렇게 보면 리뷰가 느슨해지는 상황들이 한 줄로 설명된다. 한 번 보고 말 외부 기여자, 다시 협업할 일이 없는 옆 팀의 일회성 PR, 그리고 모두가 "일단 오늘만 넘기자"는 마음이 되는 마감 전날 밤이 그렇다. 이 상황들의 공통점은 사람이 나빠서가 아니라 δ\delta 가 낮아졌다는 데 있다. 미래의 그림자가 짧아지면, 합리적인 사람일수록 먼저 협력을 접는다.

팀 단위로 보면

지금까지는 두 사람만 있는 게임을 이야기했다. 하지만 실제 팀에는 리뷰어가 여럿이고, 모두가 같은 코드베이스를 함께 쓴다. 사람이 늘어나면 게임의 모양이 조금 달라진다.

생태학자 개릿 하딘(Garrett Hardin)은 1968년 공유지의 비극이라는 글에서, 누구나 쓸 수 있는 목초지에 각자 양을 한 마리씩 더 풀면 결국 목초지 전체가 망가지는 구조를 설명했다. 양 한 마리를 더 풀 때의 이득은 나 혼자 갖지만, 풀이 줄어드는 손해는 모두가 나눠서 지기 때문이다.

코드베이스는 개발 팀의 공유지에 가깝다. 대충 승인으로 아낀 30분은 온전히 내 것이지만, 그 덕분에 섞여 들어간 버그와 엉킨 구조의 비용은 팀 전체가 조금씩 나눠서 낸다. 그리고 손해가 여러 사람에게 잘게 나뉠수록, 각자가 느끼는 손해는 작아지고 유혹 TT 는 상대적으로 커 보인다.

여기에 리뷰어가 여러 명 지정된 PR 의 함정도 겹친다. 리뷰어가 세 명이면 각자 "다른 두 명이 꼼꼼히 보겠지"라고 생각하기 쉽다. 결과적으로 세 명이 모두 가볍게 훑고 지나가면, 리뷰어가 한 명이었을 때보다 오히려 덜 꼼꼼한 리뷰가 되는 웃픈 상황이 생긴다. 그래서 필자는 리뷰어를 여러 명 붙일 때도 "이 PR 의 최종 책임 리뷰어는 누구인가"를 한 명으로 정해두는 편이 낫다고 생각한다. 책임이 나뉘지 않으면, 공유지가 아니라 각자의 마당이 되기 때문이다.

보상을 바꾸는 장치들

그렇다면 팀은 무엇을 할 수 있을까. 수식이 알려주는 방법은 두 가지다. δ\delta 를 키우거나, 경계값 T−RT−P\frac{T-R}{T-P} 를 낮추는 것이다.

먼저 경계값을 낮추는 방법부터 보자. 가장 효과가 큰 건 PR 을 작게 쪼개는 것이다. 200줄짜리 PR 을 꼼꼼히 보는 데 드는 시간은 1,200줄짜리 PR 의 몇 분의 일이니, 대충 승인으로 아낄 수 있는 시간, 즉 유혹 TT 가 줄어든다. 린트, 포매터, 타입 검사, 테스트를 CI 에 맡기는 것도 같은 효과가 있다. 사람이 확인해야 할 범위가 줄면 꼼꼼한 리뷰의 비용이 줄고, 그만큼 RR 과 TT 의 차이가 좁혀진다.

다음은 δ\delta 를 키우는 방법이다. 리뷰가 다시 돌아온다는 걸 모두가 체감하게 만드는 것이다. 리뷰어를 매번 무작위로 바꾸기보다 같은 영역을 오래 맡는 사람을 정해두면, 서로의 PR 을 반복해서 주고받는 관계가 생긴다. 리뷰를 잘해준 동료를 회고나 평가에서 언급하는 것도 미래의 보상을 눈에 보이게 만드는 장치다.

극단적으로 가면 페어 프로그래밍이 있다. 두 사람이 한 화면을 보며 함께 코드를 짜면, 리뷰는 나중에 따로 하는 일이 아니라 코드를 쓰는 순간마다 일어나는 일이 된다. 게임 이론의 말로 바꾸면 라운드 사이의 간격이 거의 0이 되고, 상대의 반응을 바로 다음 순간에 확인할 수 있으니 δ\delta 는 1에 가까워진다. 물론 모든 작업을 페어로 할 수는 없지만, 리뷰가 자꾸 형식적으로 흐르는 영역이 있다면 한 번쯤 시도해볼 만한 방법이다.

속도도 중요하다. 구글의 엔지니어링 가이드는 리뷰 요청에 응답하는 데 걸리는 시간이 길어도 하루 업무일을 넘지 않아야 한다고 권한다. 리뷰가 며칠씩 묵으면 작성자는 다음 라운드를 기다리는 대신 "어차피 아무도 안 보니까"라는 결론을 내리기 쉽다. 응답이 빠르다는 건 이 게임이 계속 진행 중이라는 신호인 셈이다.

그래서 만능일까

여기까지 읽고 나면 이런 반론이 나올 수 있다.

리뷰를 꼭 점수 따먹기처럼 봐야 하나? 동료를 위해서 꼼꼼히 보는 사람도 많은데.

맞다. 사실 이 모델은 사람을 지나치게 이기적으로 가정한다. 실제 개발자들은 점수와 상관없이 좋은 코드를 보고 싶어서, 혹은 배우고 싶어서 리뷰를 꼼꼼히 하기도 한다. 위의 표는 그런 내적 동기가 전혀 없다고 가정했을 때도 협력이 가능한지를 보여주는 최악의 시나리오에 가깝다.

또 하나의 한계는 리뷰의 목적이 버그 찾기만은 아니라는 점이다. 알베르토 바켈리(Alberto Bacchelli)와 크리스천 버드(Christian Bird)는 2013년 마이크로소프트 개발자들을 관찰하고 설문한 연구에서, 개발자들은 결함 발견을 리뷰의 가장 큰 동기로 꼽지만 실제 결과는 예상보다 결함과 관련이 적었다고 보고했다. 대신 지식 공유, 팀 전체의 인지, 대안 제시 같은 효과가 컸다는 것이다.

이 결과를 우리 모델에 겹쳐보면, 꼼꼼한 리뷰의 보상 RR 에는 버그 감소뿐 아니라 리뷰어가 배우는 몫까지 들어 있다는 뜻이 된다. 리뷰를 하면서 배우는 게 많은 팀일수록 RR 이 커지고, 경계값은 자연스럽게 내려간다. 그러니 이 모델은 리뷰를 점수 계산으로 보라는 주장이라기보다, 리뷰가 무너지는 조건을 찾는 진단 도구에 가깝다.

마치며

처음의 금요일 오후 장면으로 돌아가 보자. 1,200줄짜리 PR 앞에서 들렸던 작은 목소리는 사실 꽤 정확한 계산이었다. 유혹 TT 는 크고, 마감이 다가오니 δ\delta 는 작고, 결과적으로 대충 승인이 합리적인 구간에 서 있었던 것이다.

그래서 필자는 "리뷰를 꼼꼼히 할까 대충 할까"라는 질문이 사실 "우리 팀은 내일도 같은 사람들과 리뷰를 주고받는다는 걸 서로 믿고 있을까"라는 질문에 더 가깝다고 생각한다. 협력은 개인의 성실함만으로 유지되는 게 아니라, 미래의 그림자가 충분히 길 때 저절로 합리적인 선택이 되기 때문이다.

다음에 리뷰 문화가 느슨해졌다고 느껴진다면, 누가 게으른지를 찾기 전에 PR 이 너무 커지지는 않았는지, 리뷰가 너무 오래 묵지는 않았는지부터 살펴보자. 대부분의 경우 바꿔야 할 건 사람이 아니라 게임의 보상표다.

이상으로 코드 리뷰는 죄수의 딜레마일까 포스팅을 마친다.