구글 엔지니어가 일하는 방법과 최근의 고민들

by Joongi Kim

'구글 엔지니어는 이렇게 일한다'1라는 책을 보았다. 내가 학부생이나 대학원생이었을 때 이 책을 봤으면 그냥 '역시 구글이 잘하네' 정도의 느낌만 받았을 것 같은데(책에 나오는 20억 줄의 코드가 들어있는 전사 통합 모노리포라거나 하루에 수만 개의 커밋이 올라온다거나 이런 수치에만 현혹된 채로), 7년째 꾸준히 발전시키고 있는 코드베이스를 관리해온 역사와 최근의 모노리포 이전 작업2, 그리고 개발팀 인원이 늘어나면서 생기는 다양한 문제들을 경험하고 나니 정말.... 구구절절 감동의 도가니다. 조금 과장하자면, 내가 코드 리뷰 때 반복적으로 했던 이야기들 중 상당수를 이 책의 특정 섹션에 대한 레퍼런스로 대체할 수 있겠다는 생각이 들 정도다.

650페이지가 넘는 방대한 분량의 책이지만, 기억에 남는 포인트들을 추려서 정리해보았다.

지식 공유 (3장)

최근 우리나라에서도 업계에서 화두가 되는 것 중 하나가 매니저 입장에서 소프트웨어 엔지니어들의 심리적 안전감을 어떻게 높여줄 것인가 하는 것이다. 예를 들면 실수로 코드에 버그를 만들어 프로덕션에서 깨졌다고 해도 그것을 개인의 책임으로 돌리거나 비난하기보다는 공동의 책임으로 다루는 것도 포함된다. 이는 끊임없이 새로운 시도를 해보는 걸 장려하기 위함이며 동시에 끊임없이 새로운 기술이 나오는 업계 특성 상 학습 속도를 높이기 위함이기도 하다. 나는 어렸을 때부터 질문을 많이 하고(중학교 2학년 국어 문법 시간에는 모든 문법에 대해 내가 예외 케이스를 계속 들고와서 질문하는 바람에 친구들이 나 때문에 수업을 못 듣겠다고 할 정도였다), 내 코드를 남에게 보이는 걸 별로 부끄러워하지 않았던 편인데 이런 사람은 흔치 않다. 내가 사용하는 방법은 나도 사소한 삽질로 실수하는 사람이라는 것을 있는 그대로 투명하게 보여주고, 실수에 대해서는 비난보다는 함께 해결 방법을 찾아나가는 것이 중요하다는 인식을 강조하는 것이다. 물론 이게 한두번 말로 한다고 체화되기는 어렵겠지만, 노력이라도 하면 낫지 않을까 하고 있다.

TotT (Testing on the Toilet) 실제로 본 이야기

2008년 여름에 구글 코리아 인턴십에 지원한 적이 있었다. 여러 차례 면접을 보러 갔는데, 한번은 화장실에 갔다가 알고리즘 문제 같은 것도 있고 개발 관련 팁도 있고 이런 걸 A4 용지에 인쇄해서 화장실 여기저기 붙여놓은 걸 본 기억이 난다. 이 책에서는 이것을 개발자들에게 전사적으로 꼭 공유하고 싶은 지식들을 이메일 스팸이 되지 않도록 고심한 끝에 2006년 경 도입한 방법이라고 소개하고 있다.

C++ Tips of the Week

C++은 특히나 그 파워풀함의 반대급부로 여러 상황에서 '올바르게', '안전하게' 사용하기 까다로운 프로그래밍 언어 중 하나이다. 구글에서는 아예 자체적인 공통 라이브러리인 Abseil을 만들어 사용하고 있는데(오픈소스로도 공개되어 있다), 거기에 다양한 프로그래밍 팁들의 모음을 제공하고 있다. 책에서는 이 팁들이 실제로 코드 리뷰에서도 많이 인용된다고 전한다. 나도 코드리뷰를 하다보면 공통적으로 반복하게 되는 내용들이 생기는데, 비슷한 형태로 정리된 Python 버전의 팁 모음은 어디 없을지 궁금하다.

팀 이끌기 (5장), 성장하는 조직 이끌기 (6장)

이 챕터들은.... 뭐 그야말로 지금 닥친 문제들이라서... 오히려 뭐라고 더 코멘트할 것은 없다. 이게 머리로 이런 내용들을 알고 있는 것과 실제로 내가 개발과 매니징을 동시에 해야 하는 입장에서 균형을 맞추는 것은 큰 차이가 있는 것 같다. 이 책의 표현을 따르면 테크 리드와 엔지니어링 매니저를 동시에 하는 '테크 리드 매니저'를 하고 있는 셈인데, 구글에서도 이건 너무 힘든 일이기 때문에 백전노장으로부터 고차원적인 멘토링과 지원을 받게 한다고 한다. ...나는 누구 없나. ㅠㅠ

스타일 가이드와 규칙 (8장)

내가 pptx-tool 같은 것을 만든 것도 일관성에 대한 은근한 집착이 있기 때문이다. 나에게는 코드 스타일을 맞춘다거나 하는 일들이 의식적 판단이라보다는 본능적 위험 회피 행동에 가까운 것이다. Backend.AI에서도 코드에 기여하는 사람들이 직원들뿐만 아니라 오픈소스 컨트리뷰톤 등 외부 기여자들까지 들어오기 시작하면 어떻게 하면 굳이 말로 반복해서 설명하지 않고 자동화된 방법으로 코드 스타일을 일관되게 유지할 것인가 하는 문제가 점점 커지고 있다. 최근에는 모노리포 마이그레이션 후 전체적으로 isort를 적용하기도 했다. (모노리포에 대해서는 아래에서 또 다시 언급!) Black의 경우 내가 추구하는 코드 스타일과는 좀 다른 부분들이 있어서, 좀더 세세한 옵션을 설정할 수 있는 도구인 Yapf로 규칙을 만들어 혼자서 IDE에 적용해서 써본 바로는 나쁘지 않았더래서 그쪽으로 갈지 아니면 그냥 Black으로 엎어버릴지는 아직 고민 중이다.

코드 리뷰 (9장), Code Search (17장), Critique (19장)

이 책에서 소개하는 코드 리뷰 관련 내용의 확장판은 이 문서를 참고하면 된다. 내가 작년에 탈잉 월간코드리뷰 세미나와 공개SW 페스티벌 발표 주제로 삼았던 내용이 코드 리뷰어의 관점에서 좋은 PR을 작성하기 위한 영작 방법 및 사례와 PR의 내용 구성하는 방법에 관한 것이었다. 거기서 좀더 엔지니어링 중심적인 내용을 원한다면 딱 이 책의 내용을 참고하면 될 것 갈다. 긴 말 할 것 없이, 그냥 내가 하고 싶은 말이 다 담겨있다.

코드 리뷰 시 유용하게 활용할 수 있는 Code Search나 Critique 같은 도구는 이제 GitHub의 Code Search이나 PR 시스템이 어느 정도 구글 밖에서도 비슷한 효과를 누릴 수 있게 해준다고 생각한다. 다만 한 가지 내가 좀 다르게 사용하고 있는 부분이라면, Critique에서는 개별 커밋 자체가 그대로 리뷰의 대상이 되는데, 나는 커밋은 그보다 더 작은 변경 단위로 간주하여 하나의 PR 내에서는 작은 커밋을 장려하고 대신 PR을 병합할 때 "squash"하여 전체 저장소의 관점에서는 하나의 커밋이 되도록 한다는 점이다. 어느 것이 좋다 나쁘다라고 이야기하기는 어렵고 도구의 특성에 따른 차이로 생각된다.

하나의 커밋(혹은 PR)의 규모가 얼마가 적정한가에 대해서는 약간 논란의 여지가 있지만, 최근에는 국내 스타트업들 중에도 50줄 넘어가는 PR을 금지한다거나 이런 규칙을 도입하는 사례들이 있다. 이 책에서는 규모라는 것이 단순히 줄 수로만 판단할 수 있는 것이 아닌, 코드의 영향력까지 고려해야 한다는 점과 자동화된 도구에 의한 대규모의 변경도 자주 있다는 점을 들어(22장) 적절한 시간 안에 리뷰할 수 있는 '작은' 커밋을 권장하지만 줄 수로만 측정하는 방식을 권장하지는 않고 있다.

문서자료 (10장)

개발자들이 작성하는 문서는 어떤 종류가 있는지, 그리고 그 대상 독자들과 목적에는 어떤 것들이 있는지 잘 정리해주었다. 대략 막연한 감으로만 알고 있던 것들을 정확하게 용어와 개념을 정의해주니까 뭔가 머릿속이 싹 정리되면서, 앞으로 다른 사람들에게 이야기할 때 이렇게 표현하면 좋겠구나 싶다는 생각이 많이 들었다.

대표적으로, 문서자료의 독자 분류를 탐색자(seeker)와 배회자(stumbler)로 구분한 것을 들 수 있겠는데, 나도 내가 다른 오픈소스의 문서를 찾아보거나 MSDN 같은 문서를 어떻게 읽어왔는지 돌이켜보니 이 두 가지 패턴을 경우에 따라 바꿔가며 사용하고 있다는 것을 깨달을 수 있었다.

또 인상깊었던 부분은, 튜토리얼을 작성할 때 '독자가 수행해야 하는 모든 단계에는 번호를 붙이되, 독자의 행위에 대응하는 시스템의 동작에는 번호를 붙이지 않는다'는 원칙이다. 이것도 막연하게 감으로 이렇게 하는 게 좋다고 느끼기만 했을 뿐 명문화하여 예를 들어 표현한 것을 보니 너무 반가웠다. 내가 하고 싶었던 말이 여기 다 있네...

이 책에서 또 언급하는 것 중 하나는 technical writer의 역할과 개발·문서화 프로세스 통합의 중요성이다. Technical writer는 희소 자원이기 때문에(...) 그 리소스는 개별 팀 소속의 개발자가 잘 작성하기 어려운, API 경계나 팀·조직 경계를 넘나드는(예: 고객용) 문서를 작성하는 곳에 투입되어야 한다고 이야기한 부분이 인상적이었다. 대신 개발자들도 문서 작성 자체를 워크플로우의 일부로 진행하게끔 도구와 프로세스를 개선해야 한다는 점을 강조하고 있다. 내가 Backend.AI를 모노리포로 바꾸면서 docs 디렉토리를 한 곳에 두려고 하는 것도 마찬가지 이유이기도 하다. 이 과정에서 번역 작업과 같이 개발과 별개의 속도를 가지고 진행되는 일은 별도의 저장소로 두는 게 낫다는 의견도 있었는데 이것은 필요에 따라 별도의 미러 저장소를 두는 것으로 정리한 상태이다.

테스트 (11장 ~ 14장)

여기서 가장 인상적인 부분은 테스트의 '크기' 분류를 2개의 축으로 정의한 것과, 모의 객체를 잘 사용하는 방법에 관한 내용들이었다.

나는 지금까지 테스트 크기를 단지 단위 테스트, 기능 테스트, 통합 테스트 정도로만 구분해왔는데, 이 책에서는 이것을 2차원 축으로 나누어 테스트를 실행하는 데 필요한 자원의 크기와 검증하려는 코드 경로의 범위를 기준으로 정의한다. 기기 하나의 프로세스 1개 안에서만 실행되는 가장 작은 테스트는 아예 메모리 접근을 제외한 I/O조차 하면 안 되는 가장 강력한 제약을 가지고 있지만 가장 일관성 있게 재현 가능하고, 반대쪽 끝에 있는 거대한 테스트는 여러 개의 노드를 실제로 프로비저닝해서 다중 컴포넌트 간의 상호작용이나 상태변화를 관찰하는 것이다. 작은 단위 테스트가 80%, 중간 크기 통합 테스트가 15%, 가장 큰 종단간 테스트들은 5% 이내가 되도록 관리한다는 것은 꼭 정답이랄 수는 없겠지만 나름의 참고할만한 지표가 아닐까 싶다.

테스트의 범위 측면에서는, 상태를 관찰하는 테스트와 상호작용을 관찰하는 테스트로 구분한다. 가능하면 상태를 관찰하도록 권장하고 있는데, 그래야 코드의 리팩터링이 발생하더라도 테스트가 잘 정의된 공개 API만 사용한다면 변경될 필요가 없기 때문이다. 이것도 내가 막연하게나마 '좋은 테스트'란 무엇인가에 대해 고민하고 생각했던 점들을 말끔하게 정리해준 기분이다.

또한 나 자신도 테스트를 작성하며 고민해왔던 부분으로는 테스트 대역과 모의 객체를 언제 만들어 써야 하고 이것을 어느 정도 범위에서 활용해야 하는가에 대한 것이 있다. 이 책에서도 그 부분을 상세하게 다루어서 좋았다. 나는 막연하게, 외부 시스템에 대한 의존성을 제거하는 용도나 시간이 너무 오래 걸리는 작업을 가짜 객체로 대체하는 정도만 생각했는데, 모의 객체를 어떤 경우에 남발하면 안 되는지 충실성의 trade-off는 어떻게 되는지를 다양한 코드 예제로 설명해주어서 큰 도움이 되었다.

버전 관리와 브랜치 관리 (16장), 빌드시스템 (18장), 의존성 관리 (21장)

최근에 Backend.AI 컴포넌트 저장소 7개(앞으로 더 늘어날 듯하다)를 합쳐서 하나의 모노리포로 만들었다. 보통은 모노리포라고 하면 전사 통합 저장소를 의미하기에 Backend.AI의 사례는 엄밀한 의미의 모노리포는 아니지만(오픈소스가 아닌 프로젝트들을 공개 모노리포에 담을 수는 없으므로...), 모노리포를 선택하는 관점의 고민을 거의 똑같이 공유한다고 볼 수 있다.

Backend.AI의 모노리포 도입 배경과 엔지니어링 사례에 대해서는 올해 말에 추가로 공유할 기회가 있을 것이기에 여기에서 다 자세히 적지는 않겠지만, 이 과정에서 내·외부 의존성 관리를 위해 도입한 Pantsbuild가 구글 엔지니어들이 퇴사하고 만든 구글 내부 빌드 시스템의 재개발 버전들 중 하나라는 점이 인상적이었다. 그래서 Bazel과 그렇게 많은 부분을 공유했던 것이다. 특히 이 책을 통해 개념 정리가 된 부분은, task 기반의 빌드 시스템에서 artifact 기반의 빌드 시스템으로의 변화가 왜 필요한가 하는 것이었고, Pantsbuild를 처음 접하며 느꼈던 기존에 경험해보지 않았기에 이질적이자 동시에 정말 좋은 선택이라고 느껴졌던 디자인의 근거를 알 수 있었다. 차이점이라면, Pantsbuild는 Python 프로젝트에서 사용하기 좋도록 기본 세팅값이 잘 잡혀있다는 것과, 이 책에서 '엄격한 전이 의존성 모드'라고 불리는, 의존성을 가능한 한 직접 명시하는 방향과 달리 가능하면 의존성을 정적 분석을 통해 자동 추론하는 방향을 택하여 사람이 BUILD 파일에 직접 기입해야 하는 내용의 양을 확 줄인 것이 결정적으로 다른 철학적 선택이라는 점도 알 수 있었다.

원래 Backend.AI 모노리포를 처음 작업할 때는 git sparse checkout을 바탕으로 개별 컴포넌트를 개별적으로 CI를 돌리는 방향을 생각했는데, 금새 깨달은 것은 의존성이라는 게 함수 참조뿐만 아니라 타입을 공유하는 경우 컴포넌트를 분리하여 생각할 수 없다는 것이었다. 이 책에서 그 점을 아주 정확하게 지적하고 있는데, Java와 같은 언어에서 지원하는 의존성 shading이나 벤더링 같은 기법으로도 해결하기 어려운 문제임을 밝히고 있다. 사실 이게 Pantsbuild 같은 의존성 관리 도구를 찾아보게 된 결정적 계기였고, 다행이라면 미리 알고 고른 것은 아니었지만 이 책에서 제시하는 '원-버전 규칙'을 구현하는 데 꽤 적합한 도구라는 점이다.

모노리포를 도입할 때 기술적으로 버전 관리 시스템의 scalability에 대해서 생각해보지 않을 수 없는데, 구글은 자체적으로 Piper라는 버전 관리 시스템을 개발하는 것으로 이 문제를 해결했지만 그러한 자체 개발이 모두의 해결책이 될 수는 없는 법이다. 이 책의 저자들은 Git의 성능도 지속적으로 개선되고 있고, 최근의 오픈소스 버전 관리 도구들도 '가상 모노리포'를 지원하는 경우가 늘고 있어 앞으로 10년 정도는 그런 방향으로 발전하지 않을까 예측한다. 내가 Backend.AI에 모노리포를 도입할 때(사실 바쁜 제품 개발 일정 중에 이걸로 1개월 넘게 시간을 쓴다는 것은 엄청난 투자를 하는 셈이다) GitHub에서 여러 저장소에 걸친 PR이나 이슈들을 뭔가 한눈에 쉽게 관리할 수 있는 UI를 근시일 내에 추가해줄 것이라 예상되지 않았다는 점과3, 래블업의 규모에서는 향후 2년 내 코어개발팀이 아무리 많아도 50명을 넘지는 않을 것이라는 가정 하에, 그리고 squash-merge 기법을 사용하므로 개별 PR의 커밋은 많더라도 최종 main 트리에 병합되는 커밋의 개수는 그리 크게 늘지 않을 것이라는 가정 하에 진행하였다. 언제나 그렇듯, 현재까지의 경험을 바탕으로 새로운 시도를 하는 것이지 이것이 영원불변할 궁극의 정답이라 생각하지도 않고 상황과 도구의 지원이 변화하면 다시 멀티리포로 돌아갈 수도 있음을 이야기해두었다.

구글의 빌드 시스템은 (나는 직접 본 적은 없지만) 주변의 구글에서 일하는 선후배나 친구들의 전언으로 많이 들어보았다. 특히 내가 래블업 초기에 복잡한 의존성을 가진 TensorFlow, PyTorch와 같은 GPU 기반 컨테이너 이미지를 빌드하는 과정에서 Docker가 강제하는 빌드 과정의 선형성으로 인해 빌드 단계의 앞 부분에 발생한 변경으로 그 뒤의 과정을 무조건 재실행해야 하는 시간 낭비로 고민을 많이 했는데(지금은 multi-stage build를 활용하여 이 문제를 어느 정도 완화할 수 있다), 구글에서는 분산 빌드 워커와 빌드 캐시가 너무 잘 구축되어 있어서 그냥 의존성 걸어서 다 개별 빌드해서 합치면 되는지라 이런 종류의 문제 자체를 고민을 안 한다는 걸 듣고 너무 부러웠던 기억이 있다. Pantsbuild의 비즈니스 모델 또한 원격 빌드 캐시 서버를 제공해주는 것인데, 그것이 그러한 이유구나 싶었다.

한편 이 책에서도 외부 의존성 관리 문제는 아직까지 뾰족한 수가 없다는 것을 강조하고 있다. 아무리 최고의 빌드 시스템과 최고의 버전 관리 시스템을 가지고 있고, 심지어 사실상 무한대의 컴퓨팅 자원을 의존성 관리에 쏟아부을 수 있다고 해도 말이다. 이 부분은 upstream issue로 조금이라도 데여 봤다면 다들 공감할 것이다. 정규님께 전해듣기로는 태터앤컴퍼니 시절 자체 리눅스 배포판을 만들어 관리하려고 했을 정도로 어느 정도 데인 경험이 있는 개발자들에게는 악몽과도 같은 일이다. 앞으로 오픈소스계에서 어떠한 기술이나 아이디어들이 발전할지 기대되는 부분이기도 하다.

정리

내가 평소 해왔던 고민들이나 막연하게 누군가로부터 경험이나 조언을 듣고 싶었던 문제들, 그리고 내 나름대로 그런 문제들에 대해 주변 동료들에게 이야기해왔던 포인트들을 너무나 잘 정리해놓은 책이다. 누군가 선대에 나 대신 고민해서 이미 다 정리해둔 것을 뒤늦게 발견한 반가움과 안타까움이 교차하는 느낌이다. 당장의 주어진 기능 구현이나 버그 수정만 하는 주니어 개발자들에게는 아마 이 책의 내용이 크게 와닿지 않을지도 모르겠으나, 수 년 이상 지속된 소프트웨어 프로젝트에 참여해왔거나 리더 역할을 해본 사람이라면 누구나 한번쯤 고민하는 문제들이 종합적으로 담겨있다. 구글의 방법론이 유일한 정답이라고 할 수는 없겠지만 거기서 배울 점이 있다면 이용하지 않을 이유는 없기에, 주변 개발자들에게 이 책을 강력히 읽어보라고 권하고 싶다.


  1. 영어 원서는 온라인으로 볼 수 있다.

  2. 모노리포에 통합된 Backend.AI 코어는 Python 코드로 대략 7만줄이 넘는다. 어떤 식으로든 제품에 영향을 끼치는 외부 의존성까지 모두 포함하여 계산할 경우, 150만줄이 넘어간다.

  3. GitHub의 최신 기능 중 하나인 Project 보드(베타)를 활용하면 어느 정도 여러 저장소의 이슈나 PR들을 모아보는 것은 가능하지만, 하나의 기능 개선이나 버그 수정을 위해 여러 저장소에 각각 PR을 날리고 이것을 하나의 이슈에 연결할 경우 그중 단 하나의 PR만 병합하더라도 이슈가 완료 처리되어버리는 문제(관련 이슈 제기)를 비롯하여 실질적으로 코드 작성자나 리뷰어의 입장에서 다중 저장소 문제를 편하게 해결해주지 못했다. 즉, Git 수준에서 가상 모노리포 사용을 돕는 도구들은 존재하지만 GitHub의 작업 흐름은 아직 이에 못 미친다. 물론 프로젝트 관련 기능들이 계속 개선되면 언젠가 이런 한계들이 해결될 수도 있을 것이다.