티스토리 뷰
1. 소프트웨어 엔지니어링이란?
구글에서는 이따금 “소프트웨어 엔지니어링은 흐르는 시간 위에서 순간순간의 프로그래밍을 모두 합산한 것이다”라고 말하곤 합니다. 소프트웨어 엔지니어링에서 프로그래밍이 큰 비중을 차지하는 건 틀림없지만 프로그래밍은 결국 새로운 소프트웨어를 제작하는 수단입니다. 여러분이 이 차이를 받아들인다면 자연스럽게 프로그래밍 작업(개발, development)과 소프트웨어 엔지니어링 작업(개발, development + 수정, modification + 유지보수, maintenance)의 차이도 궁금할 것입니다. 시간이라는 요소가 더해지면서 프로그래밍에는 중요한 차원이 하나 늘어서 더 입체적으로 바뀝니다. 정육면체는 정사각형이 아니고 거리는 속도가 아니듯, 소프트웨어 엔지니어링은 프로그래밍이 아닌 것이죠.
시간이 프로그램에 미치는 영향을 알아보려면 “이 코드의 예상 수명은?” 이라는 질문을 던져보면 좋습니다. 짧게 생을 마감하는 코드는 대체로 시간의 영향을 받지 않습니다. 하지만 수명이 길어질수록 변경이라는 요소가 점점 중요해집니다. 우리가 생각하는 소프트웨어 엔지니어링과 프로그래밍을 가르는 핵심은 이 사실을 인식하는 데서 시작합니다.
소프트웨어 엔지니어링에서 만병통치약이란 찾기 어렵습니다.
[ 1.1 시간과 변경 ]
그렇다면 단명하는 프로그램용 코드와 수명이 훨씬 긴 프로젝트가 만들어내는 코드는 구체적으로 어떻게 다를까요? 수명이 길어질수록 “동작한다”와 “유지보수 가능하다”의 차이를 더 분명하게 인지해야 합니다. 불행히도 둘을 구분하는 완벽한 해법은 없습니다. 소프트웨어를 장기간 유지보수하는 일은 끝나지 않는 전쟁이기 때문이죠.
1.1.1 하이럼의 법칙
하이럼의 법칙(Hyrum’s Law): API 사용자가 충분히 많다면 API 명세에 적힌 내용은 중요하지 않습니다. 시스템에서 눈에 보이는 모든 행위(동작)를 누군가는 이용하게 될 것이기 때문입니다.
하이럼의 법칙은 최선의 의도, 최고의 엔지니어, 꼼꼼한 코드 리뷰가 뒷받침되더라도 공표한 계약(명세)이나 모범 사례를 완벽하게 구현해냈다고 단정할 수 없다는 현실을 표현한 말입니다. API 소유자는 인터페이스를 명확하게 설명해놓으면 어느 정도의 유연성과 자유를 얻을 수 있습니다. 하지만 현실에서는 API 사용자가 (명세에는 없는) 기능을 찾아 활용하기도 하며, 그 기능이 유용해 널리 쓰이면 추후 API를 변경하기 어렵게 됩니다.
따라서 변경이 얼마나 유용할 지를 분석할 때는 이러한 충돌을 조사, 식별, 해결하는 데 따르는 비용도 고려해야 합니다.
1.1.2 사례 해시 순서
“당장 돌아가야 한다”라는 생각으로 작성한 코드와 “언제까지고 작동해야 한다”라는 생각으로 작성한 코드의 차이를 생각해보면 어떤 관계인지가 분명하게 그려질 것입니다.
- 이용하는 API의 명세에 명시되지 않은, 즉 언제든 변할 수 있는 기능을 사용하는 코드는 “임시방편적인” 혹은 “기발한” 코드입니다.
- 반대로 모범 사례를 따르고 미래에 대비한 코드는 “클린하고 유지보수 가능한” 코드입니다.
1.1.3 “변하지 않기”를 목표로 하지 않는 이유
효율 개선은 상황을 더욱 복잡하게 만듭니다. 구글은 데이터센터를 비용 효율적인 장비들로 꾸리길 원합니다(특히 CPU 효율 개선에 힘씁니다). 하지만 구글에서 오래전에 만들어둔 알고리즘과 데이터 구조는 최신 장비에서 효율이 떨어지기도 합니다. 연결 리스트와 이진 검색 트리는 여전히 잘 동작하지만 CPU 클록과 메모리 지연시간의 격차가 점점 벌어지면서 “효율적인” 코드의 모습이 변하고 있습니다. 그래서 소프트웨어 설계도 제때 변경해주지 않으면 최신 하드웨어를 도입하는 효과가 퇴색됩니다.
[ 1.2 규모 확장과 효율성 ]
1.2.2 확장 가능한 정책들
우리가 좋아하는 구글 내부 정책 중 인프라팀이 인프라 변경을 안전하게 진행하게끔 보호해주는 정책이 하나 있습니다. 바로 “인프라를 변경하여 서비스가 중단되는 등의 문제가 발생하더라도, 같은 문제가 지속적 통합(Continuous Integration, CI) 시스템의 자동 테스트에서 발견되지 않는다면 인프라 팀의 책임이 아니다”라는 정책입니다. 이 정책의 이름은 “비욘세 규칙”이며, 친군하게 표현하면 “네가 좋아했다면 CI 테스트를 준비해뒀어야지”라는 뜻입니다. 공통 CI 시스템에 추가해두지 않은 테스트는 인프라 팀이 책임지지 않는다는 뜻입니다.
우리는 전문성과 공유 포럼이 조직 확장에 기여하는 바가 크다는 사실을 깨달았습니다. 엔지니어들이 포럼에 질문하고 답하는 과정에서 지식이 전파되고 새로운 전문가가 성장합니다.
[ 1.3 트레이드오프와 비용 ]
비용은 금액만을 지칭하는 게 아닙니다. 투입된 노력과 다음의 요소들까지 모두 포괄합니다.
- 금융 비용(예: 돈)
- 리소스 비용(예: CPU 시간)
- 인적 비용(예: 엔지니어링 노력)
- 거래 비용(예: 조치를 취하는 비용)
- 기회 비용(예: 조치를 취하지 않는 비용)
- 사회적 비용(예: 선택이 사회 전체에 미치는 영향)
1.3.1 사례: 화이트보드 마커
결국 엔지니어링 조직의 선택을 결정짓는 요인은 다음의 몇 가지로 압축됩니다.
- 반드시 해야 하는 일(법적 요구사항, 고객 요구사항)
- 근거에 기반하여 당시 내릴 수 있는 최선의 선택(적절한 결정권자가 확정)
의사결정이 “내가 시켰으니까”가 되어서는 안 됩니다.
1.3.3 사례: 분산 빌드
결국 구글은 자체 분산 빌드 시스템을 개발했습니다. 자체 빌드 시스템 개발에는 물론 비용이 들었죠. 시스템을 개발할 엔지니어를 투입해야 했고, 다른 모두의 컴파일 습관과 워크플로를 바꿔 새로운 시스템에 적응시키는 데는 더 많은 엔지니어 시간이 투입됐습니다. 빌드 시스템 자체를 구동할 컴퓨팅 자원도 별도로 필요했음은 물론입니다. 그래도 전체적으로는 절약되는 비용이 훨씬 컸음이 명백했습니다. 빌드가 빨라졌고 엔지니어가 멍 때리는 시간이 줄었습니다.
1.3.5 결정 재고하기와 잘못 인정하기
잘 드러나지는 않지만 잘못했음을 인정할 수 있게 해주는 능력 역시 데이터 중심 문화가 주는 커다란 장점입니다. 우리는 취합한 데이터에 기초하여 특정 시점에 결정을 내립니다. 제대로 된 데이터이고 가정의 수는 가능한 한 적어야 좋겠지만, 결정 시점에 가용한 데이터만을 활용해야 한다는 한계가 있습니다. 그래서 새로운 데이터를 얻어 상황이 바뀌거나 가정이 무너진다면 기존 결정에 오류가 있었음이 밝혀질 수도 있습니다. 혹은 당시에는 옳았지만 지금은 아닐 수도 있겠죠. 장수하는 조직에 특히 중요한 요소입니다. 시간은 기술적 의존성과 소프트웨어 시스템뿐 아니라 의사결정에 활용되는 데이터도 달라지게 하기 때문이죠.
우리는 데이터에 기초한 의사결정을 강력히 지지합니다. 하지만 데이터 자체도 시간이 지나면 변하고 새로운 데이터가 나타날 수 있음을 알죠. 그러니 시스템의 생애 동안 과거에 내린 결정을 수시로 재고해봐야 합니다.
이때 중요한 것은 결정권자에게 잘못을 인정할 권리가 있느냐입니다. 어떤 사람의 직관에는 맞지 않을지 모르지만, 잘못을 인정할 줄 아는 리더가 더 존경받습니다.
2. 팀워크 이끌어내기
사람은 완벽하지 않습니다. 그래서 인간을 “간헐적 버그들의 집합”에 가깝다고 이야기하곤 하죠. 하지만 동료에 내재된 버그를 이해하려면, 무엇보다 여러분 내면에 서식하는 버그를 먼저 이해해야 합니다.
이번 장의 핵심 주제는 소프트웨어 개발은 “팀의 단합된 노력”의 결실이라는 점입니다. 그래서 엔지니어링팀이 (혹은 어떤 형태든 창의적 협업이) 성공하려면 겸손, 존중, 신뢰라는 핵심 원칙에 맞게 여러분 자신의 행동을 바로잡아야 합니다.
[ 2.2 천재 신화 ]
구글에서의 업무는 거의 대부분이 천재 수준의 지능을 요구하지 않는 반면, 모든 업무가 최소한의 사회성을 요구합니다(다른 회사들도 대동소이합니다). 그래서 우리의 경력을 미래로 이어주는 핵심은 다른 사람과 얼마나 잘 협력하느냐입니다.
[ 2.3 숨기는 건 해롭다 ]
2.3.1 조기 감지
위대한 아이디어를 세상으로부터 숨기고 완벽히 다듬어질 때까지 아무도 들여다보지 못하게 하는 건 엄청난 도박입니다. 초기 설계에는 근본적인 실수가 스며 있기 쉽습니다. 자칫하다가는 바퀴를 다시 발명하게 될 수도 있습니다. 그리고 협업이 주는 이점도 얻지 못합니다.
그래서 피드백을 “조기에” 받을수록 이러한 위험이 크게 줄어듭니다. 검증된 주문인 “일찍 실패하고, 빨리 실패하고, 자주 실패하라”를 기억해두세요.
조기 공유의 효과는 단지 개인적인 실수를 예방하고 아이디어를 검증하는 데 그치지 않습니다. 프로젝트의 소위 “버스 지수”를 높여주기도 하죠.
2.3.2 버스 지수
버스 지수(bus factor): 몇 명의 팀원이 버스에 치어서 일을 할 수 없게 될 때 프로젝트가 망하게 되는지를 나타내는 지수
최소한 각 책임 영역마다 2차 소유자(담당자)를 두고, 제대로 된 문서를 갖춰 둔다면 프로젝트의 미래를 보장하고 버스 지수를 높이는 데 도움이 됩니다.
2.3.3 진척 속도
프로그래머는 긴밀하게 피드백받을 때 가장 효율적으로 일합니다. 함수 하나를 짜고 컴파일하고, 테스트 하나 짜고 컴파일하고, 리팩터링 살짝 하고 컴파일합니다. 이 방법이 우리가 코드를 작성하자마다 가장 빠르게 오타와 버그를 잡는 길입니다.
현재 데브옵스 철학은 “가능한 한 일찍 피드백하고, 가능한 한 일찍 테스트하고, 보안과 프로덕션 환경을 가능한 한 초기부터 고려한다”라는 목표를 천명하고 있습니다. 이 모두는 개발 워크플로를 원점 회귀(shift left)하라는 아이디어에 녹아 있습니다. 즉, 문제를 빨리 찾을수록 고치는 비용이 낮아집니다.
빠른 피드백 루프는 코드 수준뿐 아니라 전체 프로젝트 수준에서도 이뤄져야 합니다. 야심 찬 프로젝트는 빠르게 진화하기 때문에 변화하는 환경에 잘 적응해야만 합니다. 생각하지도 못한 설계 장애나 정치적인 장벽에 부딪히거나 단순히 무언가가 계확한 대로 동작하지 않을 수 있습니다. 요구사항이 기대와 다르게 바뀔 수도 있고요. 여러분이라면 계획이나 설계 변경이 필요한 시점을 즉시 알려줄 피드백 루프를 어떻게 마련할 것인가요? 정답은 팀플레이입니다.
[ 2.4 모든 건 팀에 달렸다. ]
더 간단히 말하면 “소프트웨어 엔지니어링은 팀의 단합된 노력입니다.” 다른 사람과 함께 일해야 합니다. 비전을 공유하세요. 역할을 나누세요. 다른 이로부터 배우세요. 멋진 팀을 만드세요.
2.4.1 사회적 상호작용의 세 기둥
협업의 열반에 들어가려면 가장 먼저 사회적 스킬의 세 기둥을 배우고 익혀야 합니다.
- 겸손(humility): 당신과 당신의 코드는 우주의 중심이 아닙니다. 당신은 모든 것을 알지도, 완벽하지도 않습니다. 겸손한 사람은 배움에 열려 있습니다.
- 존중(respect): 함께 일하는 동료를 진심으로 생각합니다. 친절하게 대하고 그들의 능력과 성취에 감사해합니다.
- 신뢰(trust): 동료들이 유능하고 올바른 일을 하리라 믿습니다. 필요하면 그들에게 스스로 방향을 정하게 해도 좋습니다.
여기서 얻을 수 있는 교훈은 “사회적 관계의 힘을 과소평가하지 말라”는 것입니다. 관계는 언제나 프로젝트보다 오래 지속됩니다. 동료들과 끈끈해지면 여러분이 필요할 때 기꺼이 자신들의 수고를 마다하지 않을 것입니다.
2.4.3 겸손, 존중, 신뢰 실천하기
자존심 버리기
겸손은 중요하지만 그렇다고 바짝 엎드리라는 뜻은 아닙니다. 자신감을 갖는 건 나쁠 게 없죠. 그저 모든 걸 다 아는 듯 행동하지는 말라는 뜻입니다. 더 나은 방법이 있습니다. 바로 “집단적 자존심”을 찾는 것이죠. 자신이 잘 아는 분야에 대해 걱정하는 대신 팀의 성취와 단체의 자부심을 높이려 노력하세요.
자존심은 여러 가지 모습으로 드러납니다. 그리고 많은 경우 자신의 생산성을 떨어뜨립니다.
비평하고 비평받는 법 배우기
먼저 “누군가의 창조적 산출물에 대한 건설적인 비평”과 “다른 이의 성향에 대한 맹렬한 공격”의 차이를 모두가 바르게 이해해야 합니다. 성향을 공격하는 건 쓸데없는 짓입니다. 사소하며 대응할 방법도 거의 없습니다. 건설적 비판은 프로젝트에 도움이 되며 개선을 위한 지침을 줄 수 있고, 또 주어야 합니다. 여기서 가장 중요한 점은 건설적으로 비판하는 사람은 상대방을 진심으로 생각하고 상대방의 업무가 개선되길 바라야 한다는 것입니다. 그리고 동료를 존중하는 법을 배우고 건설적이고 공손하게 비평하는 법을 배워야 합니다. 누군가를 진심으로 존중한다면 자연스럽게 재치 있고 도움되는 표현을 고르려 신경쓰게 될 것입니다. 물론 좋은 표현을 고르는 기술도 많이 연습해야 향상되겠죠.
대화의 반대편으로 가서, 여러분 자신도 비평을 잘 수용할 줄 알아야 합니다. 자신의 기술에 겸손해야 함은 물론, 상대는 내 최우선 관심사를 진심으로 생각하며 절대 나를 어리석다고 생각하는 게 아님을 믿어야 합니다. 같은 맥락에서, 우리 자존감을 우리가 작성한 코드(혹은 그 어떤 창작물이더라도)와 동일시해서는 안 됩니다.
누군가에게 “잘못했다”라고 해서는 안 됩니다. 무언가를 “고치라고 요구”해서도 안 됩니다. 마지막으로 “다른 사름들과 다르게 했다고 비난”하면 안 됩니다(바보가 된 기분이 들거든요). 이렇게 하면 상대는 즉시 방어 자세를 취하고 지나치게 감정적으로 반응할 것입니다.
빠르게 실패하고 반복하기
구글에서 제가 정말 좋아하는 좌우명은 “실패는 선택이다”입니다. 구글에서는 “가끔씩 실패하지 않는다면 충분히 혁신적이지 않거나 위험을 충분히 감수하지 않은 것이다”라는 믿음이 널리 통용됩니다. 실패를 “배우고 다음 단계로 넘어갈 수 있는 절호의 기회”라고 생각하는 것이죠.
2.4.4 비난 없는 포스트포템 문화
실패한 근본 원인을 분석하여 문서로 남기는 것이 실수로부터 배우는 핵심입니다. 이를 구글은 포스트 모템(postmortem)이라고 합니다. 포스트모템 문서가 쓸모없는 사죄, 변명, 지적으로 채워지지 않도록 각별히 주의하세요. 이건 포스트모템의 목적이 아닙니다. 제대로 된 모스트포템에는 무엇을 배웠는지와 배운 것을 토대로 앞으로 무엇을 바꿀지가 담겨야 합니다. 그런 다음 포스트모템을 쉽게 열람할 수 있고 포스트모템에서 제안한 변화를 팀이 실천하는지 확인해야 합니다. 실패를 제대로 기록해두면 다른 이들도 무슨 일이 있었는지 알 수 있고 똑같은 실수를 반복하는 일을 피할 수 있습니다.
훌륭한 포스트 모템에는 다음 내용이 담겨야 합니다.
- 사건의 개요
- 사건을 인지하고 해결에 이르기까지의 타임라인
- 사건의 근본 원인
- 영향과 피해 평가
- 문제를 즉시 해결하기 위한 조치 항목(소유자 명시)
- 재발 방지를 위한 조치 항목
- 해당 경험에서 얻은 교훈
마음을 열고 받아들이자
다른 이로부터 배우는 데 열려 있을수록 여러분의 영향력도 커집니다. 결점이 많은 사람일수록 더 강해보입니다.
경험상 고집불통 팀원의 의견이나 반대에는 더 이상 귀 기울이지 않게 되고, 대신 공인된 장애물 취급하며 피해 다닙니다. 여러분도 이런 사람이 되길 원치는 않을 테니 “다른 사람이 내 생각을 바꿔도 괜찮아”라는 생각을 항상 머릿속에 담아두길 바랍니다.
사실 결점을 드러낸다는 것은 겸손을 겉으로 표현하는 일이며, 책임을 지고 의무를 다 하려는 의지의 표출입니다. 그리고 다른 이들의 의견을 신뢰한다는 신호이기도 합니다. 그 결과 사람들은 당신의 솔직함과 용기를 존중하게 될 것입니다. 때때로 여러분이 할 수 있는 최선의 말은 “저는 잘 모르겠습니다”일 수도 있습니다.
2.4.5 구글답게 하기
구글은 “구글다움(Googleyness)”이 갖춰야 할 기준을 명확히 정의해서 이 문제를 해결했습니다. 강력한 리더십을 보이고 “겸손, 존중, 신뢰”를 드러내는 태도와 행동들을 정의한 것으로, 다음과 같습니다.
- 모호함을 뚫고 번창한다: 끊임없이 변화하는 환경 속에서도 상충하는 메시지와 방향에 잘 대처하고, 합의를 이끌어내고, 문제에 대한 진전을 이룰 수 있습니다.
- 피드백을 소중히 한다: 피드백을 주고받을 때 품위와 겸손을 유지하고 개인과 팀의 발전에 피드백이 주는 가치를 이해합니다.
- 저항(항상성)을 극복한다: 다른 이들이 저항하거나 관성 때문에 움직이지 않으려 하더라도 야심 찬 목표를 세우고 밀고 나아갑니다.
- 사용자를 우선한다: 구글 제품의 사용자 입장에서 생각하고 존중하며 그들에가 가장 도움되는 행동을 추구합니다.
- 팀에 관심을 기울인다: 동료들의 입장에서 생각하고 존중하며 팀의 결집을 위해 누가 시키지 않더라도 적극적으로 돕습니다.
- 옳은 일을 한다: 모든 일에 강한 윤리 의식을 갖고 임합니다. 팀과 제품의 진정성을 지키기 위해서라면 어렵거나 불편한 결정을 내릴 수 있어야 합니다.
3. 지식 공유
가장 중요한 사실은 조직에 배움의 문화가 자리 잡혀야 한다는 것이고, 그러려면 사람들에게 모르는 걸 인정할 수 있도록 돕는 심리적 안전을 제공해야 합니다.
[ 3.1 배움을 가로막는 장애물들 ]
조직 전체에 전문성을 공유하기란 결코 쉬운 일은 아니라서 배움의 문화가 견고하게 뒷받침하지 못한다면 여러 가지 문제에 부딪히게 됩니다. 구글은 특히 쇠가 규모가 커지면서 다음의 문제들을 겪었습니다.
- 심리적 안전 부족
- 불이익이 두려워서 스스로 위험을 감수하거나 실수를 드러내기 꺼리는 환경
- 이 현상은 두려움이 팽배한 문화 혹은 꼭꼭 숨기려는 경향으로 나타나곤 함
- 정보 섬
- 조직의 각 부서가 서로 소통하거나 자원을 공유하지 않아서 지식이 파편화됨
- 이런 환경에서는 일하는 방식을 각각의 부서가 제각기 만들어나가서 여러 현상이 나타남
- 정보 파편화
- 정보 중복
- 정보 왜곡
- 단일 장애점
- 중요한 정보를 한 사람이 독점하여 병목이 생김
- “여러분을 위해 내가 다 처리하지” 같이 좋은 의도에서 시작되기도 하지만, 단기 효율을 높여주는 대신 장기 확장성을 희생하는 꼴임(그 팀은 팀으로서 필요한 일들을 어떻게 해내야 할지 전혀 배우지 못함)
- 따라서 “전부 아니면 전무 전문성”으로 이어짐
- 전부 아니면 전무 전문성
- 조직 구성원이 “모든 것을 아는” 사람과 ”아무것도 모르는” 초심자로 나뉨
- 전문가들은 항상 모든 일을 자신들이 처리하게 되고 새로운 전문가를 키우기 위한 여력이 줄어들음
- 지식과 책임은 계속 이미 전문가가 된 사람들에게 집중되고, 새로운 팀원이나 초심자들은 그들만의 울타리에 갇혀 느리게 성장하게 됨
- 앵무새처럼 흉내내기
- 이해하지 못한 상태로 흉내만 내는 것을 말함
- 목적을 이해하지 못하고 무의식적으로 기존 패턴이나 코드를 따라함
- 유령의 묘지
- 무언가 잘못될 게 두려워서 아무도 손대지 않는 영역(주로 코드)
- 두려움과 비합리적인 의심 때문에 사람들이 손대기를 기피하는 영역임
[ 3.2 철학 ]
기록된 지식은 확장성이 좋지만, 사람이 해주는 맞춤형 도움도 장점이 큽니다. 전문가는 특정 개인에게 딱 맞는 정보가 무엇인지 가늠하여 문서에 적힌 내용이 적절한지, 그리고 그 내용을 어디에서나 찾을 수 있는지 알 수 있습니다. 설령 어디를 찾아봐야 할지는 모르더라도 누구에게 물어보면 될지는 보통 알고 있습니다.
현장 지식과 문서화된 지식은 서로를 보완해줍니다.
모든 형태의 배움에 최고인 유일무이한 지식 공유법은 존재하지 않으며, 어떻게 조합하는 게 최선일지는 조직에 따라 다릅니다.
[ 3.3 판 깔아주기: 심리적 안전 ]
심리적 안전은 학습 환경을 조성하는 데 매우 중요합니다.
먼저 자신이 이해하지 못한 게 있음을 인정해야 무언가를 배울 수 있습니다. 그러니 우리 모두는 타인의 무지를 탁하지 말고 그 솔직함을 반겨야 합니다.
배움에는 “무언가를 시도하다가 실패해도 안전하다”는 인식이 엄청나게 중요합니다. 건강한 환경에서라면 사람들은 질문을 던지고, 틀리고, 새로운 지식을 얻는 걸 편안하게 생각합니다. 우리 연구에 따르면 심리적 안전이 효과적인 팀을 이루는 데 가장 중요한 요인이었습니다.
[ 3.4 내 지식 키우기 ]
3.4.1 질문하기
초심자가 저지르는 가장 큰 실수는 무언가 막혔을 때 질문하지 않는 것입니다. 혼자서 극복해내고 싶다거나 “너무 기초적인”질문이란 소리를 듣는 게 두려워서일 수 있습니다. 혹은 “도움을 청하기 전에 최대한 노력해봐야 해”라고 생각할지 모릅니다. 이 함정에 빠지지 마세요! 여러분의 동료가 가장 휼륭한 정보 소스일 경우가 많습니다. 이 귀중한 자원을 충분히 활용하세요.
구글에서 몇 년을 일한 엔지니어라도 어떻게 해야 할지 모르는 영역이 존재하며, 전혀 문제 될 일이 아닙니다. “이게 뭔지 모르겠는데, 설명 좀 해주시겠어요?”라고 말하는 걸 두려워하지 마세요. 모르는 분야가 나오면 두려워하지 말고 성장하는 기회로 받아들이세요.
팀의 리더든 새로운 멤버든 항상 무언가 배울 게 있는 환경에서 살아야 합니다. 그렇지 않으면 더 이상 성장하지 못할 것입니다.
특히 조직의 리더들이 솔선수범해서 이런 문화를 만들어야 합니다. 그리고 “상급자라면 모든 걸 알아야 한다”라는 인식이 생겨나지 않도록 주의하세요. 우리는 많이 알면 알수록 모르는 것이 더 많음을 깨닫게 됩니다. 공개적으로 묻고 모르면 모른다고 인정한다면 다른 사람들도 점점 그렇게 변해갈 것입니다.
한편, 끈기를 가지고 상냥하게 답변해줘야 사람들이 안심하고 도움을 청하는 환경이 조성됩니다. 초기의 망설임을 극복하면 분위기를 빠르게 조성할 수 있으니 질문을 하도록 권장하고 “사소한” 질문이라도 답을 얻을 수 있도록 힘써주세요.
3.4.2 맥락 이해하기
특히 정상이 아니라고 보이는 결정에 대해서는 먼저 맥락을 찾아 이해해야 합니다. 우선 코드의 목적과 맥락을 이해하고, 그런 다음 변경하려는 방향이 여전히 더 나은지 고민해야 합니다. 더 낫다고 판단되면 고치고, 그렇지 않다면 미래에 다시 그 코드를 살펴볼 후임들을 위해 여러분이 생각한 근거를 적어두세요.
[ 3.7 조직의 지식 확장하기 ]
많은 회사에서 조직 문화를 나중에 생각해볼, 깔끔하게 딱 떨어지지 않는 “사람 사이의 문제”로 치부합니다. 하지만 구글은 코드 같은 산출물보다 문화와 환경을 첫 번째로 두고 생각해야 더 나은 결과를 얻는다고 믿습니다.
존중
지식을 공유할 때는 상냥함과 존중을 담아야 하고, 또 그래야만 가능합니다. 기술 업계에서는 “뛰어난 괴짜”를 용인하는(심지어 숭배하는) 경향이 있지만, 해로운 현상입니다. 상냥한 전문가도 얼마든지 가능하기 때문이죠. 구글의 소프트웨어 엔지니어링 직무 사다리에서 리더십 항목을 찾아보면 다음 내용을 명확히 밝히고 있습니다.
높은 수줄의 기술 리더십을 요구하지만, 모든 리더십이 기술 문제와 관련된 것은 아닙니다. 리더는 주변 사람들을 성장시키고, 팀의 심리적 안전을 개선하고, 팀워크와 협업 문화를 조성하고, 팀 내 긴장을 해소하고, 구글 문화의 가치를 설정하며, 구글을 더 활기차고 신나는 일터로 가꿔야 합니다. 괴짜는 좋은 리더가 아닙니다.
보상과 인정
동료의 업적을 인정해주는 공식적이고 손쉬운 제도는 직원들에게 계속해서 이타적이고 멋진 일들을 하도록 강한 동기를 부여합니다. 상여금보다는 동료를 인정해준다는 점이 더 중요합니다.
[ 3.8 가독성 제도: 코드 리뷰를 통한 표준 멘토 제도 ]
구글에서 “가독성제도”는 단순한 코드 가독성 이상을 의미합니다. 프로그래밍 언어 모범 사례를 전파하기 위한 구글 전사 차원의 “표준 멘토링 프로세스”를 지칭하죠.
이 리뷰의 가치에 공감한 수많은 엔지니어가 자발적으로 시간을 내어 함께 하기 시작했습니다. 오늘날에는 구글 엔지니어의 약 20%가 리뷰어 혹은 코드 작성자가 되어 가독성 인증 프로세스에 참여하고 있습니다.
3.8.1 가독성 인증 프로세스란?
구글에서 코드 리뷰는 필수입니다. 모든 변경 목록은 가독성 승인을 얻어야 합니다. 가독성 승인이란 해당 언어의 가독성 자격증이 있는 누군가가 해당 CL을 승인했다는 표시입니다.
3.8.2 가독성 인증 프로세스를 두는 이유
문제는 “들이는 비용에 비해 얻어지는 이익이 더 많은가”입니다.
가독성 제도는 “코드 리뷰가 길어진다는 단기적인 비용”과 “코드 품질 개선, 리포지터리 차원의 코드 일관성 향상, 엔지니어 전문성 향상에서 절약하는 장기적인 비용”을 의식적으로 맞바꾸는 제도입니다. 그래서 혜택의 시간 척도를 길게 잡을수록 코드의 기대 수명이 길다는 뜻이 됩니다.
5. 팀 이끌기
구글은 리더 역할을 두 가지로 구분해 생각합니다. 관리자(manager)는 사람을 이끌고 테크 리드(tech lead)는 기술과 관련한 책임을 집니다. 책임지는 대상 면에서는 차이가 크지만 두 역할에 필요한 기술은 꽤나 비슷합니다.
조종하는 사람이 없다면 엔지니어들은 값진 시간을 허비하며 그저 무슨 일이 벌어지기만을 기다리는 꼴입니다.
[ 5.1 관리자와 테크리드 혹은 둘 다 ]
거시적인 측면에서 엔지니어링 관리자는 자신이 관리하는 팀워 구성원 모두(테크 리드도 포함)의 성과, 생산성, 행복을 책임져야 합니다. 그와 동시에 팀에서 만드는 제품의 사업적 요구까지 충족시켜야 하죠. 하지만 사업적 요구와 개별 팀원의 요구가 항상 일치하는 것은 아니라서 이따금 관리자를 난처한 상황에 놓이게 합니다.
[ 5.2 개인 기여자에서 리더로 ]
5.2.1 두려워해야 할 건 오직… 전부다
사람들이 관리자가 되기 싫어하는 이유가 몇 가지 더 있습니다. 소프트웨어 개발 분야에서 가장 큰 이유는 바로 코딩할 시간이 크게 줄어든다는 점입니다. 테크 리드가 되든 엔지니어링 관리자가 되든 마찬가지입니다.
하지만 다음의 이유에서라면 테크 리드나 관리자가 되는 것도 고려해봄직 할 것입니다. 첫째, 여러분 자신의 경력 스펙트럼을 확장하는 길입니다. 여러분이 코딩에 아무리 능하더라도 혼자서 작성할 수 있는 코드 양에는 분명 한계가 있습니다. 둘째, 여러분은 사실 천부적인 관리자일지도 모릅니다. 어쩔 수 없이 프로젝트를 이끌게 된 사람 중 많은 이가 실은 자신이 사람들을 지도하고, 도와주고, 팀을 보호하고, 회사의 요구를 채우는 데 매우 뛰어나다는 사실을 깨닫곤 합니다. 결국 누군가는 이끌어야 합니다. 여러분이 안 될 이유는 없죠.
5.2.2 섬기는 리더십
엔지니어링 디렉터인 스티브 빈터는 “무엇보다도 관리하려는 충동을 이겨내야 해요”라는 아주 값진 조언을 해주었습니다. 새로 부임한 관리자는 직원들을 능동적으로 “관리”하려는 충동에 휩싸입니다. 그것이 관리자가 하는 일이잖아요? 하지만 그 결과는 보통 재앙으로 이어집니다.
“관리”병을 치료하려면 “섬기는 리더십(Servant Leadership)”을 자유롭게 응용할 수 있어야 합니다. 리더로서 여러분이 해야 할 가장 중요한 일은 팀을 떠받드는 것입니다. 섬기는 리더로서 여러분은 겸손, 존중, 신뢰의 분위기를 조성하려 힘써야 합니다. 예컨대 팀원이 혼자서는 제거할 수 없는 관료적 장애물을 치워주고, 팀이 합의에 이르도록 도와주고, 누군가 늦게까지 야근할 때는 저녁을 사주는 일이 될 수도 있습니다. 섬기는 리더는 팀이 나아가는 길 앞의 균열을 메우고 필요할 때 조언해줍니다. 자신의 손을 더럽히는 데 주저하지 않습니다. 섬기는 리더가 행하는 “관리”는 오직 팀의 기술적, 사회적 건강 관리 뿐입니다.
[ 5.3 엔지니어링 관리자 ]
전통적인 관리자는 일을 “어떻게(How)” 처리할지를 고민하는 반면, 훌륭한 관리자는 “무슨(What)”일을 처리할지를 고민합니다(그리고 어떻게는 팀을 믿고 맡깁니다).
실패는 선택이다
팀원들이 안전하다고 느끼게 해주는 일 역시 팀을 촉진하는 좋은 방법입니다. 심리적 안전을 확보해주면 사람들은 더 큰 위험도 기꺼이 감수할 것입니다. 여러분이나 다른 팀원들로부터 안 좋은 소리 들을 걱정 없이 자신에게 더 진솔해지게 되죠.
구글에서는 이렇게 말합니다. “불가능한 목표에 도전하면 실패할 가능성은 그만큼 크다. 하지만 불가능에 도전해 실패하면 성공이 확실하리라 생각한 일을 성취했을 때보다 십중팔구 큰 것을 얻는다.” 팀이 위험을 감수하는 문화를 조성하는 멋진 방법은 바로 실패해도 괜찮음을 알게 하는 것입니다.
사실 우리는 실패를 “많은 것을 아주 빠르게 배울 수 있는 기회”로 봅니다(똑같은 일에 계속 실패하는 건 논외입니다). 배움의 기회로 봐야지 손가락질하고 비난해서는 안됩니다. 빠른 실패가 좋은 까닭은 잃을 게 많지 않기 때문입니다. 늦게 실패해도 값진 교훈을 얻겠지만 늦어지는 만큼 잃는 것(주로 엔지니어링 시간)이 많아지기에 고통도 비례해서 커집니다.
앞서 이야기했다시피 구글에서는 중요한 프로덕션 실패가 있을 때마다 포스트모템을 수행합니다. 실패의 원인을 찾아 문서로 남기고 같은 일이 반복되지 않도록 일련의 예방 장치를 고안하는 것입니다. 누군가를 비난하거나 관료적인 책임을 물으려는 게 아닙니다. 오히려 문제의 핵심에 전력을 집중하여 모든 문제를 한 번에 해결하는 게 목적이죠. 매우 어렵지만 효과가 꽤 좋습니다.
개인의 성공과 실패는 조금 다른 문제입니다. 특정 개인이 이룬 성취는 팀이 보는 앞에서 칭한해주세요. 하지만 실패한 개인에게는 “개인적으로” 따로 불러서 “건설적인” 비판을 해주세요. 어떤 경우든 배울 기회로 삼고, 겸손, 존중, 신뢰라는 원칙 하에 행동하세요. 그러면 팀이 실패로부터 배우는 데 큰 도움이 될 것 입니다.
[ 5.4 안티패턴 ]
- 만만한 사람 고용하기
- 저성과자 방치하기
- 사람 문제 무시하기
- 만인의 친구 되기
- 채용 기준 타협하기
- 팀을 어린이처럼 대하기
5.4.2 안티패턴: 저성과자 방치하기
저성과자를 방치하는 일은 새로운 고성과자가 팀에 합류하는 걸 막기도 하며, 그나마 있던 팀 내 고성과자를 떠나게도 합니다. 그러다 보면 결국 스스로의 힘으로는 떠날 수 없는 저성과자로만 구성된 팀이 남게 됩니다. 마지막으로, 야러분이 저성과자를 팀에 붙들어둔다고 해서 그들에게 이로운 것도 아닙니다. 한 팀에서 잘 해내지 못하는 사람이라도 다른 팀에 가면 좋은 성과를 내는 경우도 많습니다.
저성과자 문제에 가능한 한 빠르게 대처하면 여러분은 팀원들을 돕는다는 본분에 더 충실할 수 있습니다. 저성과자에 적극적으로 대응하다 보면 의외로 작은 격려와 방향 제시가 필요했을 뿐인 경우도 많습니다. 너무 오래 지켜보기만 하면 그들과 팀의 관계가 걷잡을 수 없이 틀어져서 여러분이 도울 수 있는 길이 모두 막혀버릴 수도 있습니다.
저성과자를 지도하는 효과적인 방법에는 뭐가 있을까요? 사고로 다리를 다친 사람을 다시 일어서도록, 나아가 걷고 결국은 다른 팀원들과 함께 달리도록 도우려면 어떻게 해야 하는지 고민해보세요. 그러려면 짧은 기간의 마이크로매니징을 피하기는 어렵지만 전체적으로는 역시 겸손, 존중, 신뢰가 바탕이 되어야 합니다. 특히 존중이 중요합니다. 기간을 정하고 아주 구체적인 목표를 제시하세요. 작고 점진적이고 측정할 수 있는 목표여야 합니다. 그래야 작은 성공들을 많이 경험할 기회가 생기니까요. 그 팀원과 매주 만나서 진척 상황을 확인하고 다음 마일스톤에서 기대하는 바를 명확히 정의했는지, 그래서 성공과 실패를 바로 구분할 수 있는지 다시 확인하세요. 팀원이 잘 따라오지 못한다면 여러분과 상대 모두 긴 여정의 초기 단계에 들어섰다는 뜻입니다. 이 시점에 이르면 저성과자는 종종 미래가 보이지 않음을 인정하고 스스로 그만두거나 혹은 독하게 마음을 다지고 기대에 부응하기 위해 노력합니다. 어느 쪽이든 여러분은 저성과자와 직접 대면하여 중요하고 꼭 필요한 변화를 촉진하는 것입니다.
5.4.5 안티패턴: 채용 기준 타협하기
“A급 인재는 A급 인재를 뽑고, B급 인재는 C급 인재를 뽑는다” 스티브 잡스가 한 말입니다.
적합하지 못한 사람을 채용하면 팀 생산성 손실, 팀 스트레스, 직원 관리에 허비되는 시간, 마지막으로 해고 서류 작업과 스트레스 등의 비용을 치러야 합니다.
만약 여러분에게 팀원 채용 시 발언권이 없고 채용된 사람들이 만족스럽지 못하다면 더 뛰어난 엔지니어를 뽑아달라고 필사적으로 싸워야 합니다. 그럼에도 평균 이하의 엔지니어들이 팀에 배속된다면 어쩌면 직장을 옮겨야 할 떄일지도 모릅니다. 훌륭한 팀 구축에 필요한 자재가 없으니 여러분의 앞날은 정해졌다고 봐야 합니다.
[ 5.5 올바른 패턴 ]
5.5.1 자존심 버리기
겸손과 자신감 결여는 분명히 다릅니다. 자만에 차 있지 않더라도 자신감과 의견을 가질 수 있습니다. 어느 팀이든 자존심이 너무 강한 사람은 다루기 어렵습니다. 그 사람이 하필 리더라면 더욱 골치 아픈 일이 됩니다. 대신 여러분은 팀이라는 “집단”으로서의 자존심과 정체성을 강화해야 합니다.
신뢰는 “자존심 버리기”의 한 축입니다. 팀을 믿으세요. 팀원들의 능력과 기존에 이룬 성과들을 존중해야 한다는 뜻입니다. 새로 합류한 팀원이라도 마찬가지입니다.
팀을 마이크로매니징하지 않는다면 최전선에거 고군분투하는 실무자들이 여러분보다 세부사항을 잘 알고 있으리라고 확신할 수 있게 됩니다. 팀을 하나로 모으고 방향을 정하게 도와주는 건 여러분의 몫입니다. 하지만 목표를 이루기 위해 부품을 조립하는 구체적인 방법은 현장에서 손발을 맞춰 일하는 사람들이 결정하는 게 훨씬 낫다는 뜻이죠. 그래야 팀원들에게 주인의식도 생기고 성공(혹은 실패)에 대한 책임감도 더 크게 느끼게 됩니다. 일의 품질과 속도를 스스로 정하게 하세요.
리더 역할이 처음인 사람은 대부분 모든 사안을 올바르게 결정하고, 모든 것을 알고, 모든 질문에 답해야 한다는 강박으로 스스로를 옭아맵니다. 장담하건대 여러분은 때론 잘못된 결정을 내릴 것이며 모든 문제의 답을 알고 있지도 못합니다. 그럼에도 스스로가 완벽한 듯 행동한다면 팀원들의 존경을 빠르게 잃어갈 것입니다.
누군가가 여러분이 내린 결정에 질문을 던진다면 감사해야 합니다. 질문자의 의도는 대게 그저 여러분을 더 잘 이해하려는 것임을 잊지 마세요. 질문하기를 장려한다면 여러분은 건설적인 비판이 이루어지는 더 나은 팀의 더 나은 리더가 될 가능성이 훨씬 커집니다. 여러분이 팀으로서 이루려는 큰 그림을 잊지 마세요. 피드백을 수용하고 비판에 마음을 여세요. 자존심을 지키려는 충동을 이겨내야 합니다.
자존심 버리기의 마지막은 간단하지만 많은 엔지니어가 실천하지 못하는 일입니다. 바로 “실수했다면 사과하기”입니다. 말로만 “미안해요”라고 내뱉는 걸 말하는 게 아닙니다. 진심을 담아야 합니다. 사람들은 무언가를 망쳤으면 사과할 줄 아는 리더를 존경합니다. 일반적인 상식과 달리 사과한다고 해서 여러분이 피해를 입는 건 없습니다. 오히려 존경을 얻죠. 왜냐하면 사과한다는 것은 여러분이 성숙하고 상황을 판단할 줄 알며 겸손하다는 증거로 받아들여지기 때문입니다.
5.5.2 마음 다스리기
더 많은 사람을 이끌수록 감정은 억누르고 평정심을 유지해야 합니다. 사람들은 의식적으로든 무의식적으로든 사소한 일 하나까지도 어떻게 행동하고 반응해야 할지에 관한 단서를 윗사람, 즉 여러분에게서 얻기 때문입니다.
리더는 항상 무대 위에 있다고 생각하는 방법도 좋습니다. 주변 사람들이 여러분의 일거수일투족을 항시 두 눈 똑바로 뜨고 쳐다보고 있다는 뜻입니다. 아무리 사소한 것이라도 여러분이 내비치는 거의 모든 것이 무의식적으로 알려지고 팀에 전염됩니다.
무슨 일이 일어나도, 어떤 말도 안 되는 사건이 터져도, 아무리 큰 폭풍이 몰아쳐도 빌은 결코 당황하지 않을 것입니다. 빌은 그저 한 팔은 가슴에 올리고 다른 팔에는 턱을 괴고 문제가 정확히 무엇인지 묻습니다. 이런 태도는 상대를 진정시키고 문제를 해결하는 데 집중하게 도와주는 효과가 있습니다.
이 이야기로부터 마음을 다스리는 또 하나의 요령을 끄집어낼 수 있습니다. 바로 “질문하기”입니다. 팀원이 여러분에게 조언을 구한다는 것은 마침내 무언가를 해결해줄 기회가 찾아온 것입니다. 하지만 “직접 해결하기”는 가장 마지막에 택해야 하는 전략입니다. 조언을 구하는 사람은 보통 “여러분이 나서서” 해결해주길 원하는 게 아닙니다. 스스로 문제를 해결하는 걸 도와주길 바라는거죠. 스스로 해결하도록 이끄는 가장 쉬운 방법은 바로 “질문하기”입니다.
5.5.4 장애물 치우기
여러분이 그런 장애물들을 치워줄 수 있고 또 기꺼이 그러길 원한다는 사실을 팀원 모두가 반드시 인지하도록 만들어야 합니다.
5.5.5 선생이자 멘토되기
훌륭한 멘토라면 성장하는 팀에 발맞춰 멘티가 바우는 데 쓰는 시간과 제품 개발에 기여하는 시간의 균형을 잘 잡아줘야 합니다.
기본적으로 다음 세 가지를 요구합니다. 팀의 프로세스와 체계에 대한 경험, 다른 이에게 무언가를 설명해주는 능력, 마지막으로 멘티에게 도움이 얼마나 필요한지를 측정하는 능력입니다. 이 중 마지막이 가장 중요합니다. 설명이 과하거나 이야기를 끝도 없이 쏟아낸다면 멘티는 그냥 듣는 척만 하고 한 귀로 흘릴 것입니다.
5.5.6 명확한 목표 세우기
명호가한 목표 세우기는 많은 리더가 곧잘 잊어먹곤 하는 패턴입니다. 팀이 한 방향으로 빠르게 전진하기를 원한다면 리더가 설정한 방향을 모든 팀원이 이해하고 동의해야 합니다.
여러분의 목표가 명확하다면 우선순위를 명확히 설정해놓고, 구체적으로 무엇을 택하고 무엇을 버려야 할지는 팀이 스스로 정하도록 적시에 도와야 합니다.
물론 명확한 목표 없이도 팀은 성공할 수 있습니다. 하지만 개개인이 살짝씩 다른 방향으로 줄을 당기느라 엄청난 에너지를 낭비하게 됩니다. 여러분은 좌절하고 팀 생산성은 차츰 낮아지겠죠. 눈치 채는 게 늦어질수록 방향을 바로잡기까지 더 많은 에너지를 투입해야 할 것입니다.
5.5.7 정직하기
너무도 당연한 이야기 같겠지만 “모른다”라는 말을 잘 하지 못하는 관리자가 정말 많습니다. 자신의 약점을 드러낸다거나 정보에 뒤쳐져 있다고 여겨질 거라는 생각에 두려운 거죠. 하지만 그저 여러분도 한 명의 인간임을 말해주는 것뿐입니다. 그 이상도 이하도 아닙니다.
말하기 곤란한 피드백을 주는 건… 네. 어렵습니다. 팀원이 사고를 쳤다거나 기대한 만큼 일을 해내지 못했음을 처음 이야기할 때는 극심한 스트레스를 받습니다. 대부분의 관리자용 교재에서는 어려운 피드백을 전할 때는 “칭찬 샌드위치” 방식으로 충격을 완화하라고 가르칩니다. 칭찬 샌드위치란 다음과 같습니다.
당신은 팀의 탄탄한 구성원이자 가장 영리한 엔지니어 중 하나예요. 그래서인지 당신의 코드는 복잡하고 다른 팀원 누구도 이해하기가 거의 불가능하군요. 하지만 당신의 잠재력이 크니 여기 쩌는 티셔츠를 하나 드리겠습니다.
우리는 칭찬 샌드위치를 사용하지 말 것을 강하게 권합니다. 여러분이 필요 이상으로 가혹하고 잔인해야 한다고 생각하기 때문이 아닙니다. 겉에 둘러진 칭찬 때문에 핵심 메시지, 즉 실제로는 고쳐야 할 점을 지적한 것임을 제대로 인지하지 못하는 사람이 대부분이기 때문입니다. 여기서 다시 “존중”이 나설 차례입니다. 칭찬 샌드위치 없이 건설적인 비판을 전할 때는 상대방과 공감하고 따뜻하게 이야기해야 합니다. 상대가 비판을 듣자마자 방어적으로 변하지 않길 원한다면 친절과 공감이 아주 중요합니다.
직접적인 피드백이나 비판을 전할 때는 메시지를 정확하게 왜곡 없이 전달하는 게 핵심입니다. 상대가 방어 태세를 취하게 되면 자신이 무엇을 고쳐야 할지가 아니라, 역으로 여러분이 잘못한 점을 찾아 논쟁하려 들 것입니다.
5.5.8 행복한지 확인하기
리더로서 팀의 생산성을 장기적으로 더욱 끌어올리려면(이탈하는 팀원 줄이기 포함) 팀원들이 행복해하는지를 확인하는 데도 시간을 써야 합니다. 우리가 일해본 최고의 리더는 모두 반은 심리학자였습니다. 수시로 팀원들의 복지를 챙기고, 그들이 하는 일을 인정해주고, 일에 만족하는지 확인했죠. 한 테크 리드 매니저는 지저분하고 표 안 나는 일, 하지만 꼭 해야만 하는 일들을 스프레드시트에 정리한 다음 모든 팀원에게 고르게 분배되도록 관리했습니다. 또 다른 테크 리드 매니저는 팀이 일한 시간을 측정하여 이따금 대체 휴가를 주거나 팀 소풍을 보내주기도 했습니다. 번아웃과 피로 누적을 피하는 방법이었죠. 또 다른 리더는 팀원들과의 서먹함도 줄일 겸 일대일 면담을 하며 기술적인 이야기를 나누곤 했습니다. 그런 다음 업무를 수행하는 데 부족한 것은 없는지 일일이 챙겨주었죠. 친해진 다음에는 하는 일에 만족하는지, 다음 프로젝트로는 무얼 하고 싶은지 같은 이야기를 나눴습니다.
팀이 행복한지를 추적하는 간단한 방법을 하나 소개하겠습니다. 팀원들과의 일대일 면담 마지막에 “뭐 필요한 거 없어요?”라고 묻는 것입니다. 간단한 질문이죠. 하지만 팀원들의 생산성과 행복 증진에 필요한 것을 갖춰주는 데 아주 효과적인 마무리 멘트입니다. 물론 정말 필요한게 무엇인지를 자세히 알아보려면 신중하게 더 조사해봐야 할 수 있지만요. 이 질문을 일대일 면담 때마다 빼놓지 않고 던진다면 팀원들은 머릿속에 담아둘 것입니다. 그리고 모두의 업무를 개선해줄 일거리 목록을 준비해오는 사람도 하나둘 나타날 것입니다.
[ 5.7 그 외 조언과 요령 ]
- 위임하되, 곤란한 일은 직접 처리하자
- 여러분을 대신할 사람을 찾자
- 파도를 일으켜야 할 타이밍을 알자
- 혼란으로부터 팀을 보호하자
- 팀에 공중 엄호를 해주자
- 팀이 잘하고 있다면 칭찬하자
- 실패해도 쉽게 되돌릴 수 있는 일에는 “해보세요”라고 말하자
[ 5.8 사람은 식물과 같다 ]
누구에게 무엇이 필요한지 결정하고 제공하는 것이 리더로서 여러분이 해야 할 일 입니다. 동기부여와 방향 지시가 필요하죠.
방향지시는 명확한 편입니다. 수행해야 할 작업에 대한 기본적인 이해, 간단한 조직 관리 기술 약간, 전체 작업을 관리 가능한 단위로 쪼개는 능력 정도가 필요합니다. 이 도구들만 활용할 줄 알면 방향지시가 필요한 엔지니어에게 충분한 지침을 제공할 수 있을 것입니다.
5.8.1 내적 동기와 외적 동기
다니엘 핑크는 저서 “DRIVE 드라이브: 창조적인 사람들을 움직이는 자발적 동기부여의 힘”에서 사람들을 가장 행복하고 생산적이게 만드는 비법은 현금 다발 안겨주기 같은 외적인 동기부여가 아니라고 말했습니다. 그보다는 내면으로부터의 동기를 복돋아줘야 한다고 합니다. 다니엘이 주장하는 내적 동기부여 방법은 세 가지입니다. 바로 자율성, 숙련, 목적입니다.
자율성(혹은 주도성)은 다른 사람의 마이크로매니징 없이 스스로 행동할 수 있는 것을 말합니다. 제품에 나아가야 할 대략적인 방향만 알려주면 어떻게 도달할지는 스스로 결정합니다. 이렇게 했을 때 동기부여가 더 잘 되는 이유는 다음과 같습니다. 첫째, 제품과의 관계가 더 끈끈해집니다. 둘째, 제품에 대한 주인의식이 커집니다. 제품 성공에 팀원들이 기여하는 비중이 커질수록 성공시키려는 의지도 커질 것입니다.
숙련(혹은 전문성)의 기본적인 형태는 현재 기술 수준을 높이고 새로운 것을 배울 기회를 제공하는 것입니다. 숙련에 필요한 기회를 충분히 제공받은 사람들은 동기가 커지고, 때론 야근도 불사합니다. 결과적으로 더 강한 팀을 만들어주죠.
물론 아무 의미도 찾을 수 없는 일을 하고 있다면 제아무리 자율성과 숙련의 기회를 줘도 조금의 동기부여조차 되지 못합니다. 일의 목적을 제공해줘야 하는 이유죠. 자신이 하는 일의 목적을 인식하게 도와주면 훨씬 적극적이고 생산적으로 바뀔 것입니다.
6. 성장하는 조직 이끌기
[ 6.1 늘 결정하라(Always Be Deciding) ]
여러 팀으로 구성된 팀을 관리한다고 함은 기존보다 높은 수준에서 더 많은 걸 결정해야 한다는 뜻입니다. 여러분의 역할은 특정한 엔지니어링 문제 해결이 아니라 거시적인 전략을 짜는 것이죠. 이 높이에서 여러분이 내리는 결정 대부분은 여러 전략 사이의 트레이드오프들을 정확히 찾아내는 일입니다.
6.1.1 비행기 일화
가장 높은 수준에서 보면 리더로서의 여러분 역할은 사람들을 움직여서 어렵고 모호한 문제를 풀게 이끄는 것입니다. 리더의 역할은 “나무들 사이로 숲 전체를 보면서” 목표한 중요 나무까지로 가는 길을 찾아 엔지니어들을 안내해주는 것입니다.
- 눈가리개 찾기
- 핵심 트레이드오프 파악하기
- 결정하고 반복하기
6.1.3 핵심 트레이드오프 파악하기
정의상 중요하고 모호한 문제에는 마법 같은 “은총알”이 없습니다. 세상이 끝날 때까지 어떤 상황에서든 옳은 답이란 없습니다. “특정 상황에서의 최선의 답”이 있을 뿐이며, 그마저도 거의 언제나 몇 가지 면에서는 절충을 해야 합니다.
6.1.4 결정하고 반복하기
“트레이드오프의 지속적인 재조정”을 프로세스에 녹이지 않으면 팀은 완벽한 해법을 찾으려는 함정에 빠져 “분석 마비(analysis paralysis)” 상태에 놓이기 쉽습니다.
“이 결정대로 시도해보고 어떻게 되는지 지켜보죠. 상황을 봐서 다음 달에 변경 사항을 취소하고 다른 결정을 내릴 수도 있습니다”라며 위험을 낮추고 긴장을 줄여주세요. 이렇게 하면 팀원들은 계속해서 유연하게 사고하면서 자신들의 선택으로부터 배워갈 것입니다.
[ 6.2 늘 떠나라(Always Be Leaving) ]
“늘 떠나라”는 구글의 전 엔지니어링 디렉터인 바트 메더라다(Bharat Mediratta)의 유명한 말을 인용한 표현입니다.
이 조언의 안티패턴은 물론 리더 스스로가 단일 장애점(SPOF, Single Point Of Failure)이 되는 상황입니다. 구글에서는 이를 버스 지수라고 합니다.
6.2.1 미션: “자율주행” 팀을 만들어라
이런 형태의 자생력을 갖춘 조직으로 가꾸기 위해서는 세 가지가 필요합니다. 문제 공간을 분할하고, 하위 문제를 위임하고, 필요에 따라 반복하는 것입니다.
6.2.2 문제 공간 분할하기
리더들은 각각의 팀에 하위 문제 하나씩을 배정하곤 합니다. 하지만 이 방식에는 위험이 따릅니다. 하위 문제들은 시간이 지나면 변할 수 있는데 반해 팀은 경직된 경계에 갇혀 있어서 이 변화를 눈치 채기 어렵기 때문입니다. 그래서 가능하다면 조직의 구조를 느슨하게 관리하세요. 하위 팀들의 규모는 유동적이고, 팀원은 다른 하위 팀으로 옮길 수 있고, 상황이 변하먄 할당된 문제를 바꿀 수도 있는 식으로요. “너무 경직된”과 “너무 유연한” 사이에서 아슬아슬한 외줄타기를 해야 합니다.
[ 6.3 늘 확장하라(Always Be Scaling) ]
6.3.1 성공 사이클
성공 사이클은 거북하지만 피할 수 없습니다. 관리하기는 어렵지만 여러 팀으로 구성된 팀으로 확장하려면 반드시 거쳐야 하는 과정입니다. 문제를 압축하려면 팀 효율을 극대화할 방법을 찾아야 하며, 동시에 “자신”의 시간과 집중력을 더 커진 책임 범위에 맞춰 확장하는 방법도 배워야 합니다.
6.3.2 중요한 일 vs 급한 일
리더로 올라선 후에는 상황을 주도하는 능동형 일보다는 상황에 맞춰 대응하는 반응형 일의 비중이 커집니다. 높은 자리로 올라설수록 이런 경향이 커집니다.
“중요한 일”과 “급한 일”을 구별해야 한다라는 말을 흔히들 관리 분야의 명저자 스티븐 커비가 했다고 알려져 있는데, 사실 1954년 미국 대통령 아이젠하워의 다음 말에서 인용한 것입니다.
저에겐 두 가지 종류의 문제가 있습니다. 급한 문제와 중요한 문제. 급한 문제들은 중요하지 않고, 중요한 문제들은 절대 급하지 않습니다.
조급함은 리더인 여러분의 효율을 갉아먹는 가장 큰 적입니다. 스스로를 완전한 반응형 모드로 전환해버리면 삶 전체의 순간순간을 오로지 “급한” 일만 처리하면서 흘려보내게 됩니다. 리더로서 여러분은 “나만이 할 수 있는 일”을 처리해야 함을 잊지 마세요.
급한 일이 아니라 중요한 일에 더 몰두할 수 있는 효과적인 기술을 말씀드리겠습니다.
- 위임하자
- 따로 정기적으로 시간을 내자
- 나에게 효과가 있는 추적 시스템을 마련하자
6.3.3 공 떨어뜨리기
쌓인 공들을 세 그룹으로 나누세요. 아래 칸에는 급하지도 중요하지도 않아서 지우거나 무시할 20%의 공들을 담습니다. 중간 칸 60%에는 조금 급하거나 중요할 수도 있는 공들을 모아둡니다. 그리고 위 칸에는 아주 중요한 게 확실한 20%를 담습니다.
이제 상위 20%, 즉 여러분만이 할 수 있는 중요한 일들을 신중하게 골라낸 다음 오직 그 일들에만 집중하는 것입니다. 나머지 80%를 버릴 권한을 자신에게 부여하세요.
처음에는 말도 안 된다고 생각할지 모르지만 수많은 공을 이처럼 의도적으로 떨어뜨리다 보면 놀라운 사실 두 가지를 발견하게 됩니다. 첫째, 중간의 60%는 위임하지 않더라도 때때로 나서서 가져가는 중간 리더들이 나타납니다. 둘째, 중간 칸에 잘못 넣어둔 업무라도 “진짜” 중요한 것이라면 어떤 식으로든 다시 튀어올라 상위 20% 쪽으로 돌아옵니다.
그러면 여러분은 정말 중요한 일에만 집중하게 되어 시간과 집중력을 확장할 수 있습니다. 이렇게 해야 커져가는 책임을 감당할 수 있습니다.
6.3.4 에너지 관리하기
첫째, 나이가 들어가면서 여러분의 지구력은 전반적으로 좋아집니다. 마라톤 훈련과 마찬가지로 우리의 뇌와 몸은 세월이 흐르면서 점차 많은 체력을 축적합니다.
둘째, 리더들은 자신의 에너지를 점점 더 지능적으로 활용하는 방법을 배워갑니다. 집중력을 지속하는 어떠한 방법을 배운다는 뜻이죠. 전형적인 방법은 이렇습니다. 어떤 순간이든 남은 에너지 양을 인지하고 있다가 시의적절하게 특정한 방법으로 “충전”해주는 것입니다.
7. 엔지니어링 생산성 측정하기
[ 7.1 엔지니어링 생산성을 측정하는 이유 ]
조직이 두 배 커지면 소통 비용은 제곱으로 늘어납니다. 사업을 확장하려면 충원을 피할 수 없는데, 그에 따른 소통 비용이 너무 가파르게 늘어나는 것이죠. 그래서 조직 덩치에 비례하게 사업을 키워갈 수는 없습니다.
사업 확장 문제를 다르게 풀어볼 수도 있습니다. 개개인의 생산성을 높이는 것이죠. 조직 내 엔지니어들 각자의 생산성이 높아진다면 소통 비용 증가를 억제하면서 사업을 키울 수 있습니다.
[ 7.2 선별: 측정할 가치가 있는가? ]
- 어떤 결과를 기대하고, 왜 그런가?
- 우리 모두는 어떤 일이 일어나야 하는지에 대한 선입견을 가지고 있음
- 이 사실은 인정하고 시작하면 무의식 속에서 의도한 결과를 억지로 만들어내는 실수를 막을 수 있음
- 데이터가 기대한 결과를 뒷받침한다면 어떤 조치를 취하겠는가?
- 아무런 조치도 취하지 않을 거라면 측정의 의미가 전혀 없음
- 측정 결과에 상관없이 어차피 “현상 유지”할 계획인지를 주의해야 함
- 부정적인 결과가 나온다면 적절한 조치를 취할 것인가?
- 부정적인 결과가 나와도 결정이 바뀌지 않는 이유가 흔하며 부정적인 데이터를 압도하기 위한 또 다른 근거를 찾기도 함
- 결정권자들은 연구 결과를 알고는 싶어 하지만 이미 정해진 진로를 잘 틀지는 않음
- 이럴 경우는 측정할 가치가 없으며, 대부분 이 질문에 의해 연구가 중지됨
- 결과에 따른 조치는 누가 결정하고, 언제 수행하는가?
- 이 질문의 목적은 측정 의뢰자가 조치를 취할 권한이 있는지를 확인하는 것
- 소프트웨어 프로세스를 측정하는 궁극적인 목적은 사업적인 결정을 내리는 데 도움을 주기 위함임
- 그래서 결정을 내릴 사람이 누구이고 또 그에게 확신을 주려면 어떤 형태의 데이터가 필요한지를 이해해야 함
- 결정관자가 결과의 형태를 신뢰하지 않는다면 이 역시 측정할 필요가 없음
소프트웨어 프로세스 측정에 성공했다고 해서 가설이 옳다거나 틀렸다는 게 증명되는 건 아닙니다.
이해관계자가 측정 결과를 활용하지 않으면 측정 프로젝트는 무조건 실패한 것입니다. 그러니 소프트웨어 프로세스 측정은 결과 데이터가 구체적인 결정에 영향을 줄 경우에만 진행해야 합니다.
[ 7.3 GSM 프레임워크: 목표와 신호를 뒷받침하는 의미 있는 지표 선정하기 ]
구글은 지표를 만들 때 GSM 프레임워크를 씁니다. GSM은 목표(Goal), 신호(Signal), 지표(Metric)의 약자입니다.
- 목표(Goal)
- 측정자가 원하는 최종 결과
- 측정을 통해 이해하고 싶은 내용을 고차원 어휘로 표현하되 특정한 측정 방식을 명시해서는 안됨
- 신호(Signal)
- 원하는 최종 결과를 이루었는지 판단하는 방법
- 우리가 측정하고 싶어 하는 것이지만 신호 자체는 측정하지 못할 수 있음
- 지표(Metric)
- 신호를 대변함, 우리가 실제로 측정하는 대상임
- 이상적인 측정법은 아닐 수 있으나 충분히 가깝다고 믿는 것이여야 함
[ 7.8 조치를 취하고 결과 추적하기 ]
가장 이상적인 방식은 도구 개선입니다. 도구의 지원 없이 엔지니어들에게 업무 프로세스나 사고방식을 바꾸라고 요구하는 것은 좋지 않습니다. 올바른 데이터와 도구를 제공한다면 엔지니어들 스스로 합리적인 트레이드오프를 찾아낼 것입니다.
8. 스타일 가이드와 규칙
대부분의 엔지니어링 조직에는 내부 코드베이스를 관리하는 규칙이 있습니다. 예컨대 소스 파일을 저장하는 위치, 코드 포맷팅, 명명 방식, 패턴, 예외와 스레드 사용법 등을 규정합니다.
규칙은 곧 법입니다. 제안이나 권장사항이 아닌, 엄격하고 꼭 지켜야 하는 법입니다. 코드 전반에서 따라야 하는 강제사항이라서 꼭 필요하여 승인된 경우를 제외하고는 무시할 수 없습니다.
한편 지침은 권장사항과 모범사례를 말합니다. 따르는 편이 이득이라서 어지간하면 따르라고 권하지만, 규칙과 달리 다소 변형해 적용해도 괜찮습니다.
구글은 스타일 가이드를 프로그래밍 언어별로 관리합니다. 크게 보면 모든 가이드의 목표가 비슷합니다. 코드의 “지속 가능성”을 높이도록 이끄는 것이죠.
[ 8.1 규칙이 필요한 이유 ]
규칙을 관리하는 목표는 “좋은” 행동을 장려하고 “나쁜” 행동을 억제하기 위함입니다. 이는 주관적이고 무엇이 필요하냐에 따라 달라집니다. 그래서 가장 먼저 조직이 추구하는 가치를 파악해야 합니다.
확립된 규칙과 지침은 조직이 커지더라도 일관되게 통용되는 공통의 코딩 어휘가 되어줍니다. 어휘가 통일되면 엔지니어들은 코드를 표현하는 “형식”보다 코드에 담을 “내용”에 집중할 수 있습니다. 공통 어휘를 형성하면 언제니어들은 무의식적으로도 “좋은” 코드를 작성하는 경향이 생깁니다. 이처럼 규칙은 일상의 개발 패턴을 조직이 원하는 방향으로 슬쩍 밀어주는 역할로 폭넓게 활용됩니다.
[ 8.2 규칙만들기 ]
규칙 모음을 정의할 때 반드시 던져야 하는 질문은 “무슨 규칙이 필요하지?”가 아니라 “어떤 목표를 이루려 하지?” 입니다. 목표에 집중하면 규칙이 따라옵니다.
8.2.1 기본 원칙 안내
구글의 규칙 모음은 “규모와 시간 양쪽 측면에서 탄력적인 엔지니어링 환경이 지속되도록 하는 것”이라는 가치에 최적화되어 있습니다. 이런 배경에서 규칙들의 목표는 개발 환경의 복잡도를 관리하고 엔지니어들의 생산성을 희생하지 않는 선에서 코드베이스를 관리 가능하게끔 유지하는 것입니다.
여기서 트레이드오프가 발생합니다. 이 목적을 달성하는 데 도움되는 규칙 중 상당수가 엔지니어들의 자유를 제한합니다. 유연성이 다소 희생되어 불쾌해하는 사람도 있을 것입니다. 하지만 권위 있는 표준은 일관성을 높여주고 의견 대립을 줄여주므로 혜택이 더 큽니다.
이러한 관점에서 우리는 규칙을 만들 때 염두에 두어야 하는 중요한 원칙들을 찾아냈습니다.
- 규칙의 양을 최소화합니다.
- 조직 내 모든 엔지니어가 새로운 규칙을 익히고 적응하는 데는 비용이 듬
- 규칙이 너무 많다면 다 기억하지도 못할 것이고 새로 합류한 엔지니어가 적응하기도 어려울 것임
- 규칙 모음을 관리하기도 어렵고 비용도 커지므로 구글은 너무 자명한 규칙은 의도적으로 배제함
- 코드를 읽는 사람에게 맞춥니다.
- 코드는 작성되는 횟수보다 읽히는 횟수가 더 많으며 시간이 지날수록 차이가 벌어짐
- 구글은 “쓰기에 간편한”보다 “읽기에 간단한” 쪽에 가치를 두며, 네이밍을 서술적으로 길게 하기로 선택함
- 일관되어야 합니다.
- 구글은 코드 뿐만 아니라 화상회의 설정이나 출입카드 등 사소한 설정에 시간 낭비를 없애기 윟 ㅐ일관성을 갖춤
- 코드도 일관되게 작성되어 있다면 엔지니어들은 익숙지 않은 부분을 살펴볼 일이 생겨도 빠르게 작업할 수 있음
- 오류가 나기 쉽거나 예상치 못한 동작을 유발하는 구조를 피합니다.
- 정확하게 이해하지 못한 채 사용한다면 그 복잡성 때문에 오용하여 버그를 유발하기 쉬움
- 정확히 이해하고 사용했더라도 나중에 합류한 팀원이나 유지보수를 맡은 엔지니어가 같은 수준으로 이해하지 못할 수도 있음
- 꼭 필요하다면 실용성을 생각해 예외를 허용합니다.
- 때로는 예외가 필요함을 잘 알고 있고, 꼭 필요하다면 최적화나 실용성을 위해 예외를 허용함
[ 8.2.2 스타일 가이드 ]
언어 스타일 가이드에는 세 범주로 나눌 수 있습니다.
- 위험을 피하기 위한 규칙
- 반드시 써야 하거나 쓰면 안되는 언어 특성들에 관한 규칙들이 담져 있음
- 결정의 대부분은 세월이 흘러도 관리 가능한 코드를 작성하는 데 우선순위를 둠
- 모범 사례를 적용하기 위한 규칙
- 모범 사례를 반드시 따르도록 강제하는 규칙도 존재함 ex) 주석을 어디에 어떻게 작성할 것인가?
- 새롭거나 아직 널리 이해되지 못한 언어 기능을 제한하기도 함(기능 습득에 대한 방어선 및 모범 사례 대기)
- 일관성을 보장하기 위한 규칙
- 구글은 들여쓰기 공백 수, 임포트문 순서 등에 하나를 선택하여 끝없는 논쟁에서 벗어나 더 중요한 일로 시선을 돌릴 수 있게 했음
- 기타(가이드에 없는 것들)
- 너무 똑똑하게 짜지 말자
- 코드베이스를 포크하지 말자
- 바퀴를 다시 발명하지 말자
[ 8.3 규칙 수정하기 ]
구글의 스타일 가이드는 고정불변이 아닙니다. 세월이 흐르면 기존 결정이 내려질 당시와는 내부 사정이 달라지고 결정에 영향을 준 요인들도 변할 수 있습니다. 모든 규칙을 유용하고 최신 상태로 유지하려면 업데이트가 필요한 규칙이 무엇인지를 적시에 알아챌 수 있어야 합니다.
구글 스타일 가이드의 규칙에는 각각의 결정들을 뒷받침하는 근거를 명시해뒀습니다. 구글 스타일 가이드의 항목 대부분에는 고려 사항, 결정 과정에서 중요하게 생각한 장점과 단점, 최종 결론에 다다른 근거가 담겨 있습니다.
각 결정에 이른 근거를 문서로 남겨두면 규칙을 변경해야 할 때가 언제인지를 알아내기 쉬워진다는 이점이 있습니다. 시간이 흐르고 여건이 달라지면 과거에는 옳았던 결정이 현재는 최선이 아니게 될 수 있습니다. 결정에 영향을 준 요인들이 명확하게 기록되어 있다면 그 요인들 중 하나 이상이 변했는지 여부가 바로 규칙을 다시 평가해야 한다는 신호가 되어 줍니다.
[ 8.4 지침 ]
지침이란 구글의 엔지니어링 경험에서 선별한 지혜이자 과거로부터 배운 교훈들로부터 추린 모범사례들을 문서로 남긴 것입니다. 지침은 주로 사람들이 자주 실수하는 것 혹은 아직 익숙하지 않은 새로운 제주라서 혼란스러워하는 것들에 집중합니다. 규칙이 “반드시(must)” 지켜야 하는 것이라면 지침은 “되도록(should)” 따라야 하는 것입니다.
[ 8.5 규칙 적용하기 ]
규칙을 강제하는 방법으로는 교육과 훈련을 통한 사회적인 방법과 도구를 활용한 기술적인 방법이 있습니다. 구글은 많은 모범 사례를 숙달하게끔 도와주는 다양한 정규 교육을 운영합니다. 엔지니어들이 규칙을 인지하고 이해하도록 하기 위해 구글의 훈련 프로그램의 중심에는 코드 리뷰가 자리합니다.
9. 코드 리뷰
[ 9.1 코드 리뷰 흐름 ]
구글에서는 변경을 코드베이스에 커밋하기 전에 수행합니다. 이 단계를 프리커밋 리뷰(precommit review, 커밋 직전 리뷰)라고 합니다.
코드 리뷰의 최종 목표는 다른 엔지니어로부터 해당 변경을 적용해도 된다는 합의를 이끌어내는 것입니다.
=== 코드는 부채다 ===
우리는 코드가 그 자체로 부채임을 인정하고 잊지 말아야 합니다. 없어서는 안 될 부채이긴 하겠으나 존재만으로 어느 순간 누군가가 유지보수해야 할 대상이 되어버립니다.
새로운 기능이 필요한 상황은 아주 흔합니다. 하지만 개발을 시작하기에 앞서 정말 새로운 기능이 맞는지를 주의 깊게 살펴야 합니다. 중복 코드는 작성하는 시간 자체도 낭비지만, 사실 그 코드가 아예 존재하지 않을 때보다 관리 비용이 날이 갈수록 더 늘어가곤 합니다.
라이브러리나 유틸리티 코드는 특히 더 그렇습니다. 이상적으로는 중복 확인이 먼저 이루어져야 합니다. 그리고 무엇이 되었든 새로운 걸 설계할 때는 코드 작성 전에 관련 그룹과 대화를 나눠봐야 합니다.
[ 9.2 코드 리뷰 @구글 ]
구글에서는 어떤 변경이든 “승인”을 얻으려면 세 가지 측면에서의 리뷰를 통과해야 합니다.
- 다른 엔지니어로부터 정확성과 이해 용이성을 평가받습니다. 즉, 작성자가 의도한 작업을 코드가 적절하게 수행하는지를 봅니다. 필수는 아니지만 이 평가는 팀원이 해주는 경우가 많습니다.
- 변경되는 코드 영역을 코드 소유자로부터 적절하다는 승인을 받습니다. 변경 소유자가 코드 소유자라면 이 승인은 묵시적으로 받은 게 됩니다. 구글의 코드베이스는 트리 구조로 되어 있고, 디렉터리별 소유자들이 계층적으로 할당되어 있습니다. 소유권을 얼마나 넓게 혹은 좁게 배정할지는 각 팀이 결정합니다.
- 누군가로부터 가독성 승인을 받습니다. 변경 코드가 해당 언어의 스타일과 모범 사례를 잘 따르고 조직에서 기대하는 방식으로 작성되었는지를 검사받습니다. 변경 작성자가 가독성 인증자라면 가독성 승인 역시 묵시적으로 이루어집니다.
=== 소유권 ===
리조피토리에는 OWNERS 파일을 담고 있는 디렉터리들이 있고, 이 파일에는 디렉터리의 소유자 이름이 나열되어 있습니다.
구글에서 코드 소유자는 코드 커밋을 승인해줄 사람을 알리는 목적이지만, 여기서 끝인 아닙니다. 소유자에는 권한뿐 아니라 몇 가지 책임이 함께 부과됩니다. 예를 들어 소유자는 담당 코드들을 본인이 잘 이해하고 있거나 잘 이해하고 있는 사람을 찾아낼 줄 알아야 합니다.
이는 다양한 운영 방식의 토대가 됩니다. 예를 들어 루트 OWNERS 파일에 기록된 사람들은 대규모 변경을 책임지는 전역 승인자 역할을 맡을 수 있습니다. 이처럼 OWNERS 파일은 일종의 안내장처럼 작용하여 사람이나 도구가 특정 코드의 책임자를 찾기 쉽게 해줍니다.
[ 9.3 코드 리뷰의 이점 ]
코드 리뷰는 구글의 소프트웨어 엔지니어 모두가 반드시 따라야 하는 몇 안 되는 전사적인 프로세스입니다. 구글은 아무리 작더라도 코드베이스를 수정하는 거의 모든 변경에 코드 리뷰를 요구합니다. 이러한 강제적인 규제는 비용을 유발하고 엔지니어링 속도에도 영향을 줍니다. 코드베이스에 새로운 코드를 추가하는 속도를 늦추고 필요한 변경을 제때 반영하기 어렵게 할 수도 있습니다.
하지만 잘 설계된 코드 리뷰 프로세스와 코드 리뷰를 중요하게 다루는 문화가 주는 대표적인 이점은 다음과 같습니다.
- 코드가 정확한지 확인해줍니다.
- 변경된 코드를 다른 엔지니어도 잘 이해합니다.
- 코드베이스가 일관되게 관리됩니다.
- 팀이 소유권(주인의식)을 더 강하게 느낍니다.
- 지식이 공유됩니다.
- 코드 리뷰 자체의 기록이 남습니다.
9.3.1 코드 정확성
많은 연구에서 코드 리뷰가 소프트웨어 버그를 예방하는 효과가 있다고 이야기합니다. IBM의 한 연구에서는 결함을 초반에 잡아낼수록 나중에 발견해 고칠 때보다 시간이 덜 든다고 합니다. 코드 리뷰에 들이는 시간은 테스트, 디버그, 회귀 테스트에 투입되는 시간을 줄여줍니다. 물론 코드 리뷰 프로세스 자체를 단순화하여 가볍게 유지해야만 합니다. 사실 이게 핵심입니다. 프로세스가 무겁거나 확장하기 어렵다면 코드 리뷰를 지속할 수 없습니다.
정확성 평가가 주관적으로 흘러가지 않도록 하기 위해 일반적으로 변경 작성자가 선택한 방식을 존중해줍니다. 설계 측면에서든 기능 측면에서든 마찬가지입니다. 리뷰어는 자신이 선호한다는 이유로 다른 안을 주장해서는 안 됩니다. 몰륜 리뷰어가 대안을 제시하는 건 가능하지만 이해하기 더 쉽거나 기능을 개선하는 대안일 경우에만 그리 해야 합니다. 구글은 새로운 코드가 “완벽하다”고 합의될 때까지 기다리지 않고 코드베이스를 개선한다고 인정되면 변경을 승인하도록 안내합니다. 코드 리뷰의 속도를 높이는 수단 중 하나입니다.
코드 리뷰 프로세스에서 결합을 찾는 일은 여전히 “원점 회귀(shift left)” 전략에 필수입니다. 원점 회귀란 문제를 가능한 한 일찍 찾아 해결함으로써 결함으로 인한 비용을 최소로 낮추는 전략입니다.
9.3.2 코드 이해 용이성
작성자의 관점에 치우치지 않은 피드백을 제공하는 일입니다. 코드는 작성되는 횟수보다 읽히는 횟수가 몇 배는 많으므로 이해하기 쉽게 작성하는 게 매우 중요합니다.
리뷰어의 의문 하나하나는 시간이 지날수록 가치가 몇 배는 커질 것이므로 질문을 하는 게 맞다고 생각합니다.
9.3.3 코드 일관성
코드는 일정한 표준을 따라야 하며 그래야 조직 안에서 이해되고 유지보수될 수 있습니다. 너무 복잡해져서도 안 됩니다. 코드가 단순해야 다른 이들이 이해하기 쉽고 유지보수하기도 쉽습니다.
코드 작성은 단 한 번일지라도 앞으로 수십, 수백, 심지어 수천 번은 읽힐 것입니다. 따라서 코드베이스 전체의 코드를 일관되게 관리하면 모든 엔지니어의 이해도가 향상되고, 심지어 코드 리뷰 프로세스에도 긍정적인 영향을 줍니다. 그래서 때로는 일관성을 위해 기능성을 희생해야 할 때도 있습니다.
9.3.4 심리적, 문화적 이점
코드 리뷰는 문화적으로도 중요한 이점을 제공합니다. 소프트웨어 엔지니어에게 코드는 “자신의 것”이 아니라 협업을 통해 만들어지는 “조직의 공동 소유물”임을 인식시켜주는 효과입니다. 코드 리뷰를 하지 않는다면 대다수 엔지니어가 자연스럽게 각자의 취향대로 소프트웨어를 설계하게 됩니다. 코드 리뷰는 작성자가 다른 사람의 피드백을 받아들이고, 또 더 큰 이익을 위해 적절히 타협하도록 이끌어줍니다.
코드 리뷰가 선물하는 또 다른 심리적 이점은 검증입니다. 가장 뛰어난 엔지니어조차 가면 증후군을 겪거나 자기비판이 너무 심할 수 있습니다. 코드 리뷰는 그들의 작업 결과를 검증하고 인정해주는 효과가 있습니다.
코드 리뷰를 필수라고 못박으면 작성자들에게 자신의 코드를 한번 더 들여다보게 하는 효과가 생깁니다.
9.3.5 지식 공유
정보가 교환되며 지식 공유를 촉진시킵니다. 실제로 많은 코드 리뷰에서 양방향 정보 교환이 이루어집니다.
[ 9.4 코드 리뷰 모범 사례 ]
- 공손하고 전문가답게
- 작게 변경하기
- 변경 설명 잘쓰기
- 리뷰어는 최소한으로
- 가능한 한 자동화하기
10. 문서자료
현실적으로 대부분의 문서자료는 소프트웨어 엔지니어 스스로가 작성해야 합니다. 따라서 엔지니어가 문서화를 효과적으로 할 수 있도록 도와주는 적절한 도구와 보상이 필요합니다. 그 비결은 바로 현재 개발 워크플로에 긴밀하게 통합되고 조직의 성장에 발맞춰 확장되는 프로세스와 도구입니다.
구글에서 문서자료를 개선하고자 해본 시도 중 가장 성공적이었던 방법은 문서자료를 코드처럼 취급하여 엔지니어링 워크플로에 통합하는 것이었습니다. 그 결과 엔지니어들이 간단한 문서자료를 작성하고 유지보수하는 일이 한결 수월해졌습니다.
[ 10.1 문서자료란? ]
문서자료란 엔지니어가 작업을 끝마치기 위해 작성해야 하는 모든 부수적인 텍스트를 의미합니다. 별도로 작성한 문서뿐 아니라 코드 주석까지 포함된다는 뜻입니다. (사실 구글에서 엔지니어가 작성하는 문서자료 대부분은 코드 주석입니다).
10.2 문서자료가 필요한 이유
테스트에 대한 투자와 마찬가지로 문서자료에 들인 노력도 날이 갈수록 커집니다. 문서자료는 단 한 번만 작성하면 되지만 결국 수백 번, 수천 번 읽히게 됩니다. 초기 비용은 미래의 모든 독자에게 혜택으로 돌아갑니다.
예를 들어 다음과 같은 질문들에 답을 제시해주죠.
- 이 설계를 택한 이유가 뭐지?
- 코드를 왜 이런 식으로 구현했을까?
- ‘내가’ 왜 이렇게 구현했지?
[ 10.3 문서자료는 코드와 같다 ]
코드처럼 문서에도 소유자가 있습니다. 소유자가 없는 문서는 점점 낡아져서 유지보수하기 어려워집니다.
[ 10.4 독자를 알자 ]
엔지니어들이 문서자료를 작성하며 범하는 가장 중요한 실수는 자신만을 위해 쓴다는 것입니다.
하지만 실제로는 그 문서자료의 독자는 사내의 모든 엔지니어와 외부 개발자까지 상당히 다양할 수 있습니다.
그래서 문서자료를 작성하기 전에 만족시켜야 할 독자가 누구인지를 공식적으로든 비공식적으로든 알아내야 합니다.
10.4.1 독자 유형
우리는 오랜 경험에서 문서를 “짧게” 쓰는 게 유리하다는 사실을 알아냈습니다. 복잡한 주제라면 익숙하지 않은 사람을 위해 충분히 설명하되 전문가를 짜증나게 해서는 안 됩니다. 문서를 짧게 쓰려면 때로는 모든 정보를 담아 길게 쓴 다음 간명하게 편집하고 중복된 정보를 삭제하는 과정을 거쳐야 합니다. 지루한 작업 같겠지만 여기에 들이는 비용이 여러분의 문서자료를 읽는 모든 이에게 돌아간다는 사실을 잊지 마세요.
[ 10.5 문서자료 유형 ]
- 참조용 문서자료(코드 주석 포함)
- 설계 문서
- 구글의 팀 대부분은 중요한 프로젝트에 착수하기 전에 설계 문서부터 승인받아야 합니다.
- 좋은 설계 문서라면 설계의 목표와 구현 전략을 설명하고 설계 상의 핵심 선택들과 관련한 트레이드오프를 명시해야 합니다.
- 튜토리얼
- 프로젝트 환경을 새로 구축하는 과정을 담은 튜토리얼이 아주 중요합니다.
- 마땅히 아무런 사전지식 없는 엔지니어라도 “실제로” 무언가를 동작시켜볼 수 있는 “Hello World” 문서가 꼭 필요합니다.
- 어떠한 사전 설정, 권한, 도메인 지식도 가정하지 마세요.
- 개념 설명 문서자료
- 개념을 더 명확하게 전달하는 게 목적이라서 정확성을 다소 희생할 수 있음, 핵심은 독자를 이해시키는 것
- 랜딩 페이지
- 랜딩 페이지의 목적을 명확히 인식하고 자세한 정보는 모두 다른 페이지를 가리키는 링크로 대체하면 됨
- 랜딩 페이지가 교통경찰 이상의 일을 한다면 제 역할을 못하고 있다는 신호입니다.
[ 10.6 문서자료 리뷰 ]
일반적으로 문서자료 역시 리뷰를 거쳐야 합니다. 내 문서자료가 제대로 동작하는지 “테스트”하고 싶다면 리뷰해줄 사람을 섭외해야 합니다. 기술 리뷰 문서에 효과적인 방식은 크게 세가지입니다.
- 정확성 확인용 기술 리뷰: 주로 해당 주제 전문가가 수행하며, 주로 팀 동료에 해당함
- 명확성 확인용 독자 리뷰: 주로 도메인을 잘 모르는 사람이 수행하며, 주로 새로운 동료나 고객 등에 해당함
- 일관성 확인용 작문 리뷰: 주로 테크니컬 라이터나 지원자가 수행함
[ 10.9 문서화 철학 ]
엔지니어링 문서자료의 품질을 개선하려면 엔지니어들과 전체 엔지니어링 조직이 “내가 곧 문제이자 해결책임”을 깨우쳐야 합니다. 문서에서 손을 놓고 포기하지 말고, 양질의 문서자료를 생산하는 일 역시 내게 맡겨진 과업이며 장기적으로 시간과 노력을 절약해준다는 사실을 깨달아야 합니다.
11. 테스트 개요
개발 주기에서 버그를 발견하는 시기가 늦어질수록 고치는 비용이 커집니다. 많은 경우 기하급수적으로 커지죠. 하지만 “버그 잡기”는 테스트를 하는 여러 이유 중 하나일 뿐입니다. 예를 들어 “소프트웨어가 변화할 수 있도록 지원”하는 역할 역시 버그 잡기에 못지않게 중요합니다.
반족 주기가 짧은 회사는 변화하는 기술, 시장 상황, 고객 취향을 더 빠르게 받아들일 수 있습니다. 테스트 체계가 잘 갖춰져 있다면 변화를 두려워할 이유가 없습니다. 따라서 테스트 체계를 소프트웨어 개발의 핵심 역량으로 취급할 수 있습니다. 시스템을 더 빠르게 변경하고 싶다면 더 빠르게 테스트하는 방법을 모색해야 합니다.
한편, 테스트를 작성하는 행위가 시스템의 설계도 개선해줍니다. 테스트는 시스템 코드의 첫 번째 고객이라는 자격으로 여러분이 선택한 설계에 관해 많은 이야기를 해줍니다.
[ 11.1 테스트를 작성하는 이유 ]
“자동 테스트”라고 하는 것의 정체는 정확히 무엇일까요? 가장 단순한 테스트는 다음 요소들로 정의할 수 있습니다.
- 테스트하려는 단 하나의 행위(주로 메서드나 API)
- 특정한 입력(API에 전달하려는 값)
- 관측 가능한 출력 혹은 동작
- 통제된 조건(하나의 격리된 프로세스 등)
간단한 테스트가 수백에서 수천 개 모이면(test suite, 테스트 스위트) 제품이 전체적으로 의도한 설계대로 잘 작동하는지, 혹은 언제 그렇지 못한 지를 이야기할 수 있게 됩니다.
테스트는 엔지니어에게 신뢰를 줄 때만 가치가 있다는 사실을 잊지 마세요. 테스트가 생산성을 떨어뜨리고 고칠 게 계속 나오거나 결과를 믿을 수 없다면 엔지니어들은 더 이상 테스트를 신뢰하지 않고 우회 방법을 찾으려 할 것입니다. 나쁜 테스트 스위트는 테스트가 아예 없는 것만 못합니다.
11.1.1 구글 웹서버 이야기
최고의 팀은 팀원들의 집단 지성을 팀 전체의 이익으로 환원하는 방법을 찾아냅니다. 바로 자동 테스트가 하는 일이죠. 개별 엔지니어가 작성한 테스트는 팀이 공유하는 자원 풀에 추가됩니다. 따라서 팀원 모두가 공유된 테스트를 수행하고 결함을 찾아낼 수 있습니다. 디버깅 방식에서는 버그가 발생할 때마다 엔지니어가 디버거를 실행해 문제를 분석해내야 합니다. 둘 사이의 엔지니어링 비용 차이는 하늘과 땅만큼 벌어집니다.
11.1.3 작성하고, 수행하고, 조치하라
테스트를 작성하는 것과 “좋은 테스트”를 작성하는 것은 별개입니다.
자동 테스트의 핵심은 같은 동작을 끊임업이 반복하는 데 있습니다. 사람은 무언가 잘못됐을 때만 개입합니다. 테스트 절차까지 “실행 가능한 코드”로 작성해두면 코드가 변경될 때마다, 즉 하루에 수천 번도 더 돌려볼 수 있습니다. 사람과 달리 기계는 결코 지치거나 지루해하지 않으니까요.
11.1.4 테스트 코드가 주는 혜택
구글은 테스트에 투자하는 게 개발자 생산성 향상시키는 중요한 이유를 몇 가지 발견했습니다.
- 디버깅 감소
- 자신 있게 변경
- 더 나은 문서자료
- 더 단순한 리뷰
- 사려 깊은 설계
- 고품질의 릴리스를 빠르게
[ 11.2 테스트 스위트 설계하기 ]
우리는 모든 테스트 케이스에는 두 가지 독립된 요소가 있다는 결론에 이르렀습니다. 바로 크기와 범위입니다. 크기는 테스트 케이스 하나를 실행하는 데 필요한 자원을 뜻합니다. 메모리, 프로세스, 시간 등이죠. 한편 범위는 검증하려는 특정한 코드 경로를 뜻합니다. 어떤 코드를 단순히 실행했다고 해서 그 코드가 기대대로 동작하는지 검증했다고 볼 수는 없습니다. 크기와 범위는 서로 연관되어 있지만 분명 다른 개념입니다.
11.2.1 테스트 크기
구글에서는 모든 테스트를 크기 기준으로 분류하며, 엔지니어들에게 주어진 기능 조각을 검사하는 가능한 한 작은 테스트를 작성하라고 독려합니다.
테스트의 크기를 가늠하는 기준은 코드 줄 수가 아닙니다. 대신 어떻게 동작하고, 무엇을 하고, 얼마나 많은 자원을 소비하는지로 평가합니다. 작은 테스트는 프로세스 하나에서 동작하고, 중간 크기 테스트는 기기 하나에서, 큰 테스트는 자원을 원하는 만큼 사용해 동작합니다. 구글은 전통적인 용어인 “단위 테스트”와 “통합 테스트” 대신 이 정의를 사용합니다. 우리가 테스트에서 바라는 품질은 바로 속도와 결정성이기 때문입니다.
- 작은 테스트
- 단 하나의 프로세스(또는 스레드)에서 실행되어야 함
- 데이터베이스와 같은 제 3의 프로그램을 수행해서도 안됨
- sleep, I/O 연산 같은 블로킹 호출을 사용해서도 안됨(네트워크와 디스크에 접근 불가)
- 이러한 경우에는 테스트 대역을 사용해야 함(빠른 속도를 위해)
- 중간 크기 테스트
- 여러 프로세스와 스레드를 활용할 수 있고, 로컬 호스트로의 네트워크 호출같은 블로킹 호출도 가능함
- 블로킹 호출로 인해 운영체제나 서드파티 프로세스에 의존하게 되고, 성능과 결정성을 스스로 보장할 수 없다는 뜻임
- 대신 외부 시스템과의 통신은 여전히 불허함(단 한 대의 기기에서 수행되어야 함)
- 큰 테스트
- 로컬 호스트 제약에서 해방되어 여러 대의 기기를 활용할 수 있음
- 여러 시스템을 네트워크로 연결해 다루게 되면서 단일 기기에서 구동할 때보다 느려지거나 비결정성이 커질 가능성이 훨씬 증가함
- 그래서 종단간(end-to-end) 테스트를 통해 설정을 검증하거나 혹은 테스트 대역을 사용할 수 없는 레거시 컴포넌트를 테스트할 때 등으로 한정해서 사용함
- 공통 속성
- 모든 테스트는 밀폐되어야 하며, 필요한 모든 정보를 담고 있어야 함
- 테스트 수행 순서 같은 외부 환경에 관해서는 가능한 한 아무것도 가정하지 않아야 함(DB 의존 X)
- 테스트 자체를 검사해주는 테스트는 따로 없으므로 정확성이라는 중요한 검사는 사람이 직접 해야 하므로 제어문 등의 사용을 지양해야 함
11.2.2 테스트 범위
테스트 범위란 주어진 테스트가 얼마나 많은 코드를 검증하느냐를 말합니다.
다양한 크기와 범위의 테스트가 시스템 아키텍처와 조직의 현실에 맞게끔 조화롭게 혼합되어 있다면 좋은 테스트 스위트라고 할 수 있습니다.
11.2.3 비욘세 규칙
비욘세 규칙은 전체 코드베이스의 변경을 책임지는 인프라팀이 자주 활용합니다. 가령 인프라가 수정되어 A팀의 제품이 제대로 동작하지 않는 일이 벌어졌다고 해보죠. 이 경우 A팀이 작성하여 CI에 등록해둔 테스트를 모두 통과했다면, 이 문제를 수정하고 문제를 검증하는 테스트를 추가할 책임은 전적으로 A팀에 있습니다(변경을 진행한 인프라 팀이 아닙니다).
[ 11.3 구글 규모의 테스트 ]
구글은 모든 코드를 모노레포, 즉 하나의 레포지토리에서 관리합니다. 구글이 운영하는 모든 제품과 서비스의 거의 모든 코드가 한 곳에 담겨 있습니다. 20억 라인이 넘는 코드가 말이죠.
구글의 코드베이스에서는 매주 약 2500만 라인이 변경됩니다. 그중 절반 가량은 수만 먕에 이르는 엔지니어들의 손가락에서 시작되며, 나머지는 자동화 시스템이 변경합니다.
코드베이스를 열어두면 코드베이스를 모두가 함께 책임지는 공동 소유 의식이 싹틉니다.
11.3.1 대규모 테스트 스위트의 함정
자동 테스트가 엉망으로 작성되어 있다면 코드를 변경하기 어렵습니다. 특히 깨지기 쉬운 테스트, 즉 예상 결과를 너무 세세하게 표현하거나 광범위하고 복잡한 상용구가 덕지덕지한 품질 낮은 테스트는 심지어 해당 테스트와 관련 없는 코드가 변경되어도 실패할 수 있습니다. 날이 갈수록 “리팩터링해서 코드베이스가 항상 건실하도록 챙겨보자”라는 팀원의 목소리가 수그러들 것입니다.
깨지기 쉬운 테스트를 만드는 주범으로 모의 객체 오용을 들 수 있습니다. 구글의 코드베이스는 한때 모의 객체 프레임워크를 오용하여 큰 난리를 치러서 “다시는 모의 객체를 쓰지 않겠어!”라고 선언한 엔지니어들도 생겨났습니다. 이건 좀 극단적인 반응이긴 하지만, 어쨌든 모의 객체의 한계를 이해해두면 잘못 사용하는 일이 많이 줄어들 것입니다.
그 외에도 테스트 스위트가 커지면 수행 시간이 길어진다는 점도 문제입니다. 느려질수록 수행 빈도는 자연스럽게 줄어들 것이고, 테스트의 가치는 그만큼 작아집니다.
테스트 스위트가 비결정적이고 느려지면 생산성을 갉아먹습니다.
거대한 테스트를 잘 관리하는 비결은 바로 테스트를 존중하는 문화입니다. 엔지니어들이 테스트에 관심을 갖도록 장려하세요.
[ 11.5 자동 테스팅의 한계 ]
모든 종류의 테스트를 다 자동화할 수는 없습니다. 예컨대 검색 결과의 품질을 테스트하려면 사람의 판단이 개입되어야 합니다. 구글은 실제로 쿼리를 날리고 결과에 관한 느낌을 기록하는라는 연구를 내부적으로 수행합니다. 비슷한 예로 전화나 영상 통화 시스템의 성능을 평가할 때는 종종 사람에게 판단을 맡깁니다.
창의력이 필요한 분야에서도 인간이 더 뛰어날 수 있습니다. 예를 들어 복잡한 보안 취약점을 찾는 일입니다.
이를 더 일반화한 용어가 탐색적 테스트입니다. 검사 대상을 마치 고장내야 할 퍼즐로 취급합니다. 의외의 데이터를 입력하거나 예상치 못한 절차로 조작하여 망가뜨리려 시도하죠.
구글은 되도록 작은 테스트를 추구하며, 좁은 범위 테스트를 추구합니다. 실제로 비즈니스 로직 대부분을 검증하는 좁은 범위의 단위 테스트가 80%, 둘 이상의 구성요소 간 상호작용을 검증하는 중간 범위의 통합 테스트가 15%, 전체 시스템을 검증하는 종단간 테스트가 5% 정도가 되도록 합니다.
12. 단위테스트
단위의 범위를 잘 정해서 어디까지가 공개 API인가를 정하는 일에 과학적인 정답은 없습니다. 그래도 다행히 쓸만한 경험법칙은 있습니다.
- 소수의 다른 클래스를 보조하는 용도가 다인 메서드나 클래스라면 독립적인 단위로 생각하지 않는 게 좋습니다. 따라서 이들은 직접 테스트하지 말고 이들이 보조하는 클래스를 통해 우회적으로 테스트해야 합니다.
- 소유자의 통제 없이 누구든 접근할 수 있게 설계된 패키지나 클래스라면 직접 테스트해야 하는 단위로 취급해야 합니다. 이때도 테스트는 사용자와 똑같은 방식으로 접근하는 것입니다.
- 소유자만이 접근할 수 있지만 다방면으로 유용한 기능을 제공하도록 설계된 패키지나 클래스(지원 라이브러리) 역시 직접 테스트해야 하는 단위로 봐야 합니다. 이 경우 라이브러리용 자체 테스트와 라이브러리 사용자 테스트가 존재한다는 점에서 다소 중복이 생길 수 있지만, 유익한 중복입니다.
요구사항이 변하지 않는 한 테스트를 수정할 필요 없게 하려면 내부 구현을 위한 코드가 아닌 공개 API를 호출하면 됩니다. 즉, 테스트가 시스템을 사용자와 똑같은 방식으로 사용하는 것입니다.
테스트가 사용자에게 유용한 예제 코드와 문서자료가 되어준다는 보너스도 얻을 수 있습니다. 어디까지가 “공개 API”이냐가 항상 명확한 것은 아니며, 일부 프로그래밍 언어에서 말하는 가시성과는 다를 수 있습니다.
구글에서 말하는 단위 테스트는 단일 클래스나 메서드처럼 범위가 상대적으로 좁은 테스트를 뜻합니다.
테스트의 가장 중요한 목적은 물론 버그 예방입니다. 그 다음은 엔지니어의 생산성 개선입니다. 범위가 더 넓은 테스트들과 비교하여 단위테스트는 생산성을 끌어올리는 훌륭한 수단이 될 수 있는 특성을 많이 지니고 있습니다.
- 작은 테스트는 빠르고 결정적이어서 수시로 실행하며 피드백을 즉각 얻을 수 있습니다.
- 대상 코드와 동시에 작성할 수 있을 만큼 작성하기 쉽습니다. 따라서 커다란 시스템을 설정하거나 이해할 필요 없이 작성 중인 코드를 검증하는 데 집중할 수 있습니다.
- 빠르게 작성할 수 있으므로 테스트 커버리지를 높이기 좋습니다. 커버리지가 높다면 기존 동작을 망가뜨리지 않으리라는 확신 속에서 코드를 변경할 수 있습니다.
- 개념적으로 간단하고 시스템의 특정 부분에 집중하므로 실패 시 원인을 파악하기 쉽습니다.
- 대상 시스템의 사용법과 의도한 동작 방식을 알려주는 문서자료 혹은 예제 코드 역할을 해줍니다.
엔지니어들은 단위 테스트를 엄청나게 많이 실행하게 됩니다. 단위 테스트는 엔지니어의 일상에서 비중이 크기 때문에 구글은 테스트 유지보수성을 상당히 중시합니다. 유지보수하기 쉬운 테스트는 “그냥 작동하는(jsut work)” 테스트를 말합니다. 즉, 한번 작성해두면 실패하지 않는 한 엔지니어가 신경 쓸 필요 없고, 혹 실패한다면 원인을 바로 알 수 있는 진짜 버그를 찾았다는 뜻입니다.
[ 12.2 깨지기 쉬운 테스트 예방하기 ]
12.2.1 변하지 않는 테스트로 만들기 위해 노력하자
우리는 엔지니어가 제품 코드를 변경하는 유형을 생각해보고, 그 유형 별로 테스트가 어떻게 대응해야 하는지를 따져봐야 합니다.
- 순수 리팩터링
- 외부 인터페이스만 놔두고 내부만 리팩터링한다면 테스트는 변경되지 않아야 함(성능 및 가독성 개선 등)
- 테스트의 역할은 리팩터링 후에도 시스템의 행위가 달라지지 않았음을 보장하는 것
- 리팩터링 시에 테스트를 변경해야 한다면 시스템의 행위가 달라졌거나, 테스트가 세부 구현에 지나치게 의존하고 있었거나
- 새로운 기능 추가
- 새 기능을 검증할 테스트를 새로 작성해야 하며, 기존 테스트들은 변경되지 않아야 함
- 기능을 추가했는데 기존 테스트를 변경해야 한다면 검증하는 기능에 의도치 않은 영향을 주었거나 테스트 자체에 문제가 있다는 뜻임
- 버그 수정
- 버그가 존재한다는 것은 기존 테스트 스위트에 빠진 게 있다는 신호임
- 버그 수정과 동시에 바로 그 누락됐던 테스트를 추가해야 함
- 버그 수정 때도 통상적으로 기존 테스트들은 변경되지 않아야 함
- 행위 변경
- 시스템의 기존 행위를 변경하는 경우로, 기존 테스트 역시 변경되어야 함
- 이 경우 테스트를 변경한다는 것은 시스템이 한 약속을 “의도적”으로 변경하는 것이며, 앞의 세 유형은 “의도치 않게” 변경한 것임
- 저수준 라이브러리라면 애초부터 변경할 일이 없게끔 설계하는 데 엄청난 노력을 기울임
요점은 행위 수정 외에는 기존 테스트를 손볼 일이 없어야 한다는 것입니다.
기존 테스트를 수정해야 하는 경우는 시스템의 행위가 달라지는 파괴적인 변경이 일어날 때뿐입니다. 그리고 이런 상황에서의 테스트 갱신 비용은 모든 사용자의 코드를 갱신하는 비용보다 대체로 저렴합니다.
12.2.2 공개 API를 이용해 테스트하자
요구사항이 변하지 않는 한 테스트를 수정할 필요 없게 해주려면 내부 구현을 위한 코드가 아닌 공개 API를 호출하면 됩니다. 즉, 테스트가 시스템을 사용자와 똑같은 방식으로 사용하는 것입니다.
테스트가 사용자에게 유용한 예제 코드와 문서자료가 되어준다는 보너스도 얻을 수 있습니다.
공개 API만 이용하는 테스트라면 더 현실적이고 잘 깨지지 않습니다. 시스템이 명시한 규약을 따르기 때문이죠.
어디까지가 "공개 API"이냐가 항상 명확한 것은 아니며, 이는 프로그래밍 언어에서 말하는 가시성과는 다를 수 있습니다. 단위의 범위를 잘정해서 어디까지가 공개 API인가를 정하는 일에 과학적인 정답은 없습니다. 그래도 다행히 쓸만한 경험법칙은 있습니다.
- 소수의 다른 클래스를 보조하는 용도가 다인 클래스(예: Helper 클래스) 라면 독립된 단위로 생각하지 않는 게 좋습니다. 따라서 이런 메서드나 클래스는 직접 테스트하지 말고 이들이 보조하는 클래스를 통해 우회적으로 테스트해야 합니다.
- 소유자의 통제 없이 누구든 접근할 수 있게 설계된 패키지나 클래스라면 거의 예외 없이 직접 테스트해야 하는 단위로 취급해야 합니다. 이때도 테스트는 사용자와 똑같은 방식으로 접근하는 것입니다.
- 소유자만 접근할 수 있지만 다방면으로 유용한 기능을 제공하도록 설계된 패키지나 클래스 (예: 지원 라이브러리) 역시 직접 테스트해야 하는 단위로 봐야 합니다. 이 경우 지원 라이브러리의 코드를 라이브러리 자체용 테스트와 라이브러리 사용자용 테스트 모두에서 검사한다는 점에서 다소 중복이생길 수 있지만 유익한 중복입니다.
12.2.3 상호작용이 아니라 상태를 테스트하자
테스트가 내부 구현에 의존하는 대표적인 유형이 또 있는데, 이는 어떤 메서드를 호출하느냐가 아니라 호출 결과를 어떻게 검증하느냐와 관련됩니다. 시스템이 기대한 대로 동작하는지 검증하는 방법은 크게 두 가지입니다. 상태 테스트는 메서드 호출 후 시스템 자체를 관찰하며, 상호작용 테스트는 호출을 처리하는 과정에서 시스템이 다른 모듈들과 협력하여 기대한 일련의 동작을 수행하는지를 확인합니다.
대체로 상호작용 테스트는 상태 테스트보다 깨지기 쉽습니다. 이유는 우리가 진짜로 원하는 것은 결과가 "무엇"이냐지만, 상호작용 테스트는 결과에 도달하기까지 시스템이 "어떻게" 작동하냐를 확인하려 듭니다.
상태 테스트는 우리가 무엇에 관심이 있는지를 더 정확하게 표현합니다. 시스템이 어떤 상태에 놓이는가죠.
잠재적으로 문제가 될 수 있는 상호작용 테스트가 만들어지는 가장 큰 원인은 모의 객체 프레임워크(mocking framework)에 지나치게 의존하기 때문입니다. 테스트 대역을 쉽게 만들 수 있고, 테스틑 대역은 자신을 향한 모든 호출을 기록하고 검증할 수 있게 해줍니다. 그래서 우리는 진짜 객체가 빠르거 결정적이라면 테스트 대역을 지양하고 진짜 객체를 사용해야 합니다.
[ 12.3 명확한 테스트 작성하기 ]
언젠가는 테스트가 실패할 것이고, 실패란 좋은 것입니다. 테스트 실패는 엔지니어에게 유용한 신호를 주며 단위 테스트의 존재 가치를 증명하는 가장 주요한 수단 중 하나입니다.
테스트가 실패하는 이유는 크게 두 가지 입니다.
- 대상 시스템에 문제가 있거나 불완전합니다. 테스트는 정확히 이 문제를 잡아낼 목적으로 설계된 것입니다. 실패 이유가 이것이라면 버그를 고치라는 경로로 보면 됩니다.
- 테스트 자체에 결함이 있을 수 있습니다. 이 경우는 대상 시스템에는 아무런 문제가 없습니다. 기존 테스트가 이런 이유로 실패했다면 깨지기 쉬운 테스트라는 뜻입니다. 깨지기 쉬운 테스트를 완전히 제거하기는 거의 불가능합니다.
테스트가 실패하면 먼저 어느 경우인지 조사해야 합니다. 이 일을 얼마나 빠르게 마치냐는 테스트의 명확성(clarity)에 달렸습니다. 명확한 테스트라 함은 존재 이유와 실패 원인을 엔지니어가 곧바로 알아 차릴 수 있는 테스트를 말합니다. 명확한 테스트는 대상 시스템의 문서자료 역할을 해주고 새로운 테스트를 작성하기 쉽게 도와주는 토대가 되어주는 등의 이점도 제공합니다.
테스트는 작성자가 팀을 떠난 후에도 계속 이용될 가능성이 크므로, 테스트의 명확성은 시간이 흐를수록 더욱 중요해집니다.
12.3.1 완전하고 간결하게 만들자
완전성과 간결성은 테스트를 명확하게 만드는 데 도움되는 거시적인 특성입니다. "완전한 테스트(complete test)"란 결과에 도달하기까지의 논리를 읽는 이가 이해하는 데 필요한 모든 정보를 본문에 담고 있는 테스트를 말합니다. "간결한 테스트(concise test)"란 코드가 산만하지 않고, 관련 없는 정보는 포함하지 않는 테스트입니다.
코드 공유는 완전성과 간결성을 높여줄 거입니다. 또한 테스트를 더 명확하게 만들 수 있다면 DRY 원칙을 거스르는 게 나을 때도 많습니다.
"테스트 본문에는 테스트를 이해하는 데 필요한 정보를 모두 담아야 하며, 그와 동시에 눈을 어지럽히거나 관련 없는 정보는 담지 않아야 합니다."
12.3.2 메서드가 아니라 행위를 테스트하자
많은 엔지니어가 본능적으로 테스트의 구조를 대상 코드의 구조와 일치시키려고 합니다. 제품 코드의 메서드 하나에 테스트 메서드도 하나씩 두는 식이다. 이 방식은 대상 메서드가 복잡해질수록 테스트도 함께 복잡해져서, 실패해도 원인을 파악하기 어려워진다.
public void displayTransactionResults(User user, Transaction transaction) {
ui.showMessage(transaction.getItemName() + "을(를) 구입하셨습니다.");
if (user.getBalance() < LOW_BALANCE_THRESHOLD) {
ui.showMessage("경고: 잔고가 부족합니다!");
}
}
이런 코드가 있다면 테스트 하나에서 두 메시지를 모두 검증하려는 모습을 흔히 찾아볼 수 있습니다.
@Test
void testDisplayTransactionResults() {
processor.displayTransactionResults(
new User(LOW_BALANCE_THRESHOLD.plus(dollars(2)),
new Transaction("물품", dollars(3))
);
assertThat(ui.getText()).contains("물품을(를) 구입하셨습니다.")
assertThat(ui.getText()).contains("경고: 잔고가 부족합니다!")
}
대상 메서드가 더 복잡해지고 더 많은 기능을 구현한다면 이 단위 테스트 역시 계속 복잡해지고 커져서 다루기가 점점 까다로워질 것이다.
더 나은 방법은 테스트를 메서드 별로 작성하지 말고 행위별로 작성하는 방법입니다. 여기서 행위(behavior)란 특정 상태에서 특정한 일련의 입력을 받았을 때 시스템이 보장하는 "반응"을 뜻합니다. 행위를 때로는 given/when/then으로 표현하기도 합니다.
메서드와 행위는 다대다 관계입니다. 사소하지 않은 메서드 대부분은 여러 가지 행위를 담당하며, 어떤 행위는 이런 메서드를 연계해야 완성됩니다.
다음은 메서드 중심 테스트를 행위 주도 테스트로 다시 작성해본 모습입니다.
@Test
void testDisplayTransactionResults_showItemName() {
processor.displayTransactionResults(
new User(),
new Transaction("물품", dollars(3))
);
assertThat(ui.getText()).contains("물품을(를) 구입하셨습니다.")
}
@Test
void testDisplayTransactionResults_showLowBalance() {
processor.displayTransactionResults(
new User(LOW_BALANCE_THRESHOLD.plus(dollars(2)),
new Transaction("물품", dollars(3))
);
assertThat(ui.getText()).contains("경고: 잔고가 부족합니다!")
}
테스트를 쪼개느라 코드가 늘어났지만, 각 테스트가 훨씬 명확해졌으니 결과적으로 그 이상의 값어치를 한 것입니다. 행위 주도 테스트는 메서드 중심 테스트보다 명확합니다.
- 자연어에 더 가깝게 읽히기 때문에 힘들여 분석하지 않아도 자연스럽게 이해할 수 있음
- 테스트 각각이 더 좁은 범위를 분석하기 때문에 원인과 결과가 더 분명하게 드러남
- 각 테스트가 짧고 서술적이어서 이미 검사한 기능이 무엇인지 더 쉽게 확인할 수 있음
- 테스트의 구조는 행위가 부각되도록 구성하자(given, when, then)
- 테스트 이름은 검사하는 해우이에 어울리게 짓자(검사하려는 행위를 보여줘야 함)
행위 중심의 패턴을 무너뜨리는 가장 큰 원흉은 대상 시스템을 호출하는 코드 사이사이에 추가되는 단정문 때문입니다. 이럴 때는 when과 then 블록을 교대로 정의하는 방법도 있습니다. 또한 긴 블록을 쪼갠 후 and 접속사로 연결해주면 더 잘 읽힙니다.
이런 테스트를 작성할 때는 동시에 여러 행위를 검사하는 실수를 범하지 않도록 주의해야 합니다. 테스트 각각은 단 하나의 행위만 다뤄야 하며, 따라서 절대다수의 단위 테스트에는 when과 then 블록으 하나씩이면 충분합니다.
12.3.3 테스트에 논리를 넣지 말자
테스트가 명확하면 검토하기도 쉽습니다. 복잡성은 대체로 논리라는 형태로 나타납니다. 논리는 프로그래밍 언어에서 명령형 요소를 이용해 표현합니다. 논리가 포함된 코드의 결과를 예측하려면 약간의 정신 노동을 거쳐야 합니다. 코드를 그냥 훑는 것만으로는 부족하죠. 더욱이 테스트에서는 논리가 조금만 들어가도 추론하기가 어려워집니다.
12.3.4 실패 메시지를 명확하게 작성하자
잘 작성된 실패 메시지라면 테스트의 이름과 거의 동일한 정보를 담고 있어야 합니다. 즉, "원하는 결과", "실제 결과", "이때 건네진 매개변수의 값"을 명확히 알려줘야 합니다. 또한 기대한 상태와 실제 상태를 명확히 구분해주고 결과가 만들어진 맥락 정보도 더 제공해야 합니다.
[ 12.4 테스트와 코드 공유: DRY가 아니라 DAMP! ]
대부분의 소프트웨어는 "반복하지 말라"라는 뜻의 DRY 원칙을 숭배합니다. DRY는 개념들을 각각 독립된 하나의 장소에서 구현하여 코드 중복을 최소로 줄이면 유지보수하기 더 쉽다고 말합니다. 이 원칙대로 프로그래밍하면, 특히 기능을 변경해야 할 때 단 한 곳의 코드만 수정하면 끝이므로 아주 유용합니다. 물론 이렇게 모아두면 참조에 참조를 따라가야 실제 로직을 구현한 코드를 찾아 분석할 수 있기 때문에 코드 명확성이 떨어진다는 단점도 생깁니다.
물론 코드 변경과 관리를 쉽게 해준다는 장점에 비하면 사소한 단점입니다. 하지만 테스트 코드에서는 DRY가 주는 혜택이 그리 크지 않습니다. 그와 동시에 테스트는 복잡해지면 손해가 막심합니다. 테스트는 정확성을 스스로가 보장하지 못하면 버그가 생길 위험이 커집니다. 다시 말하지만, 테스트가 복잡해져서 테스트를 검증할 테스트가 필요하다고 느껴지기 시작하면 무언가 잘못된 것입니다.
DRY를 고집하는 대신 테스트 코드는 DAMP가 되도록 노력해야 합니다. DAMP는 "서술적이고 의미 있는 문구(Descriptive And Meaningful Phrase)"를 뜻합니다. 단순하고 명료하게만 만들어준다면 테스트에서 다소의 중복은 괜찮습니다.
DAMP 방식으로 테스트를 짜면 중복된 코드도 보이고 테스트 본문도 더 길어졌습니다만, 그만한 가치가 있습니다. 테스트 각각이 훨씬 더 의미 있어졌고 테스트 본문만 봐도 전체를 이해할 수 있게 되었기 때문이죠.
DAMP가 DRY를 대체하지는 않습니다. 보완해주는 개념입니다. 도우미 메서드와 테스트 인프라는 테스트를 더 명확하게 만드는 데 여전히 도움을 줄 수 있습니다. 가령 본문 코드를 더 간결하게 해주고 검사하려는 행위와 관련 없이 반복되는 세세한 단계들을 추상화해줄 수 있습니다.
여기서 핵심은 테스트에서의 리팩터링은 반복을 줄이는 게 아니라 더 서술적이고 의미있게 하는 방향으로 이루어져야 한다는 점입니다.
12.4.2 공유 셋업
셋업 메서드가 이용한 특정 값에 의존하는 테스트가 생겨나기 시작하면 악몽이 시작될 수 있습니다. 해당 값이 어디서 왔는지 읽는 이가 찾아내야 하므로 완벽하지 않아 보입니다.
특정 값을 요구하는 테스트라면 그 값을 기술해줘야합니다. 필요하다면 셋업 메서드가 정의한 기본값을 덮어써야 하죠.
12.4.3 공유 도우미 메서드와 공유 검증 메서드
모든 테스트가 마지막에 도우미 메서드를 호출하는 것은 아주 위험한 습관입니다. 이런 식으로 해서는 테스트를 행위 주도적으로 만들기가 어렵습니다. 테스트 각각의 의도를 추론해내기가 훨씬 어렵기 때문입니다. 그리고 버그가 하나 발생하면 여러 테스트가 동시다발로 실패할 때가 많아서 살펴봐야 할 범위를 좁혀내기가 쉽지 않습니다.
하지만 하나의 목적에 집중하는 검증 메서드는 여전히 유용합니다. 잘 만들어진 검증용 도우미 메서드는 여러 조건으 확인하는 게 아니라 입력에 대한 단 하나의 "개념적 사실"만을 검증합니다. 개념적으로는 단순하지만 그 개념을 검사하는 로직이 복잡한 경우라면 특히 큰 도움이 됩니다.
12.4.4 테스트 인프라 정의하기
가끔은 다른 테스트 스위트와도 코드를 공유하면 유용할 때가 있습니다. 구글은 이런 종류의 코드를 "테스트 인프라"라고 부릅니다. 테스트 인프라는 주로 통합 테스트나 종단간 테스트 때 빛을 발합니다. 그리고 신중하게 설계한다면 특정 상황에서는 단위 테스트를 작성하는 데도 큰 도움을 줍니다.
테스트 인프라는 독립된 제품 대우를 해줘야 하며, 마땅히 자신을 검사해줄 "자체 테스트들을 갖추고 있어야" 합니다.
우리는 엔지니어가 제품 코드를 변경하는 유형을 생각해보고, 그 유형 별로 테스트가 어떻게 대응해야 하는지를 따져봐야 합니다.
- 순수 리팩터링
- 외부 인터페이스만 놔두고 내부만 리팩터링한다면 테스트는 변경되지 않아야 함(성능 및 가독성 개선 등)
- 테스트의 역할은 리팩터링 후에도 시스템의 행위가 달라지지 않았음을 보장하는 것
- 리팩터링 시에 테스트를 변경해야 한다면 시스템의 행위가 달라졌거나, 테스트가 세부 구현에 지나치게 의존하고 있었거나
- 새로운 기능 추가
- 새 기능을 검증할 테스트를 새로 작성해야 하며, 기존 테스트들은 변경되지 않아야 함
- 기능을 추가했는데 기존 테스트를 변경해야 한다면 검증하는 기능에 의도치 않은 영향을 주었거나 테스트 자체에 문제가 있다는 뜻임
- 버그 수정
- 버그가 존재한다는 것은 기존 테스트 스위트에 빠진 게 있다는 신호임
- 버그 수정과 동시에 바로 그 누락됐던 테스트를 추가해야 함
- 버그 수정 때도 통상적으로 기존 테스트들은 변경되지 않아야 함
- 행위 변경
- 시스템의 기존 행위를 변경하는 경우로, 기존 테스트 역시 변경되어야 함
- 이 경우 테스트를 변경한다는 것은 시스템이 한 약속을 “의도적”으로 변경하는 것이며, 앞의 세 유형은 “의도치 않게” 변경한 것임
- 저수준 라이브러리라면 애초부터 변경할 일이 없게끔 설계하는 데 엄청난 노력을 기울임
13. 테스트 대역
테스트 대역(test double)은 실제 구현 대신 사용할 수 있는 객체나 함수를 말합니다. 영화를 찍을 때 위험한 장면에서는 주연 배우 대신 스턴트맨이나 스턴트우먼이 연기해주는 것과 비슷합니다. 테스트 대역을 사용하는 것을 “모킹한다(mocking)”라고도 하지만 이번 장에서 이 용어는 피하겠습니다. 목은 테스트 대역 중 한 형태인 모의 객체를 지칭할 때도 쓰기 때문입니다.
테스트 대역은 실제 구현보다 훨씬 가벼워서 이럴 때마저도 빠르고 안정적인 작은 테스트로 대응할 수 있게 도와줍니다.
[ 13.1 테스트 대역이 소프트웨어 개발에 미치는 영향 ]
- 테스트 용이성(testability)
- 테스트 대역을 사용하려면 코드베이스가 테스트하기 쉽도록 유연하게 설계되어 있어야 합니다.
- 테스트를 염두에 두지 않고 설계된 코드라면 상당히 많이 리팩터링해야 할 것입니다.
- 적용 가능성(applicability)
- 테스트 대역을 제대로 활용하면 엔지니어링 속도가 크게 개선되겠지만, 잘못 사용하면 오히려 깨지기 쉽고 복잡하고 효율도 나쁜 테스트로 전락합니다.
- 특히 커다란 코드베이스 곳곳에서 테스트 대역을 잘못 사용한다면 이러한 단점이 배가되어 엔지니어 생산성을 크게 떨어뜨립니다.
- 실제로 테스트 대역을 활용하기에 적절하지 않은 경우가 많으니 되도록 실제 구현을 이용하길 권장합니다.
- 충실성(fidelity)
- 충실성은 테스트 대역이 실제 구현의 행위와 얼마나 유사한지를 말합니다.
- 테스트 대역이 실제 구현과 전혀 다르게 동작한다면 그 대역을 이용한 테스트들은 별다른 가치를 만들어내지 못합니다.
- 한편 100% 충실한 것 역시 현실적이지 않습니다. 테스트에 활용하려면 대역은 실제보다 훨씬 단순해야 합니다. 많은 상황에서 완벽하게 충실하지 못한 대역만으로도 테스트는 충분한 효과를 얻습니다.
[ 13.2 테스트 대역 @ 구글 ]
구글이 어렵게 깨우친 교훈 하나는 테스트 대역을 쉽게 만들어주는 모의 객체 프레임워크를 과용하면 위험하다는 것입니다. 구글이 모의 객체 프레임워크를 처음 도입했을 시기에는 정말이지 만능 요술램프처럼 보였습니다. 의존하는 다른 모듈들에 신경 쓰지 않고 원하는 코드 조각에 집중하는 테스트를 매우 쉽게 만들 수 있었죠. 이런 테스트를 셀 수 없이 양산하며 몇 해가 지나자 커다란 대가를 치르게 되었습니다. 테스트를 작성하기는 쉬웠지만 버그는 잘 찾아내지 못했고 끊임없이 보수해야 했죠. 그래서 우리는 방향을 틀었습니다. 오늘날에는 많은 엔지니어가 모의 객체 프레임워크를 피하고 실제에 더 가까운 테스트를 작성합니다.
[ 13.4 테스트 대역 활용 기법 ]
- 가짜 객체(fake object)
- 제품 코드로는 적합하지 않지만 실제 구현과 비슷하게 동작하도록 가볍게 구현한 대역
- 인메모리 데이터베이스가 좋은 예시임
- 가짜 객체가 없다면 새로 작성해야 하는데, 이는 “미래의 행위까지도 비슷하게 흉내 내야”해서 쉽지 않음
- 스텁(stub)
- 원래는 없던 행위를 부여하는 과정으로, 대상 함수가 반환할 값을 지정하는 것을 반환값을 스텁한다고 함
- 스텁은 보통 모의 객체 프레임워크를 이용함
- 스텁은 빠르고 쉽게 적용할 수 있는 기술이지만 한계가 있음
- 목(mock) 또는 상호작용 테스트(interaction test)
- 대상 함수를 실제로 호출하지 않고도 그 함수가 “어떻게” 호출되는지를 검증하는 기법
- 함수가 올바른 방식으로 호출되지 않으면 실패하는 테스트가 있을 수 있음
- 스텁과 마찬가지로 모의 객체 프레임워크를 활용함
[ 13.5 실제 구현 ]
구글은 가능하다면 대상 시스템이 의존하는 실제 구현을 사용합니다. 코드가 프로덕션 환경에서와 동일하게 동작해야 테스트 충실성이 높아지는데, 실제 구현을 이용하면 자연스럽게 그렇게 됩니다.
그 전에는 모의 객체 프레임워크를 과용했고, 그러다 보니 테스트 결과와 실제 구현의 동작이 달라지는 일이 반복되었습니다. 리팩터링하기도 점점 어려워졌습니다.
실제 구현을 선호하는 테스트 방식을 고전적 테스트(classical test)라고 하며, 반대로 모의 객체 프레임워크를 선호하는 테스트 방식은 모의 객체 중심주의 테스트(mockist test)라고 합니다.
13.5.1 격리보다 현실성을 우선하자
구글은 현실적인 테스트를 선호합니다. 대상 시스템이 올바르게 동작한다는 확신을 높여주기 때문이죠. 단위 테스트들이 테스트 대역에 너무 의존한다면 엔지니어가 통합 테스트를 추가로 수행해보거나 수동으로 직접 동작시켜봐야 같은 수준의 확신이 생길 것입니다. 이러한 추가 작업은 개발 속도를 늦춥니다. 그 뿐만 아니라 추가 작업에 시간이 너무 들 것 같아서 엔지니어들이 그냥 무시하기로 한다면 버그가 숨어들 위험이 커집니다.
13.5.2 실제 구현을 사용할지 결정하기
빠르고 결정적이고 의존성 구조가 간단하다면 실제 구현을 사용하는 게 좋습니다. 예컨데 값 객체(value object)라면 실제 구현을 사용해야 합니다. 금액, 날짜, 주소, 혹은 리스트나 맵 같은 컬렉션 클래스가 대표적인 값 객체입니다.
이보다 복잡한 코드라면 실제 구현을 사용하는 게 비현실적일 때가 많습니다. 이제부터 이야기할 고려사항들을 염두에 두고 판단하시기 바랍니다.
- 실행 시간
- 속도는 단위 테스트에서 가장 중요한 특징 중 하나입니다.
- 단위 테스트는 개발 과정 내내 아주 빈번하게 실행되면서 코드에 문제가 생기면 빠르게 알려주는 게 주된 목적이므로, 실제 구현의 수행 시간이 오래 걸린다면 테스트 대역이 유용할 수 있습니다.
- 실제 구현을 사용하면 빌드가 오래 걸린다는 트레이드오프가 있습니다.
- 결정성
- 같은 버전의 시스템을 대상으로 실행하면 언제든 똑같은 결과를 내어주는 테스트를 결정적인 테스트라고 하며, 항상 성공하거나 항상 실패해야 한다는 뜻입니다.
- 결과가 자주 튄다면 일관되게 동작하는 테스트 대역을 통해 테스트의 충실성을 높일 수 있습니다.
- 실제 구현은 테스트 대역보다 훨씬 복잡할 수 있어서 비결정적일 여저가 더 많습니다.
- 통제할 수 없는 외부 서비스나 시스템 클록에 의존하는 코드는 비결정성의 주범으로 꼽힙니다.
- 의존성 생성
- 실제 구현을 이용하려면 의존 대상들도 모두 생성해야 하지만, 테스트 대역은 다른 객체를 별로 사용하지 않습니다.
- 제품 코드가 팩터리 메서드나 자동 의존성 주입을 지원한다면 테스트에서도 똑같이 이용하는 게 가장 좋습니다.
- 이처럼 객체 생성 코드를 테스트에서도 이용하려면 미리부터 유연하게 만들어놔야 합니다.
[ 13.6 속이기(가짜 객체) ]
실제 구현을 이용할 수 없을 때는 가짜 객체가 최선일 경우가 많습니다. 가짜 객체는 실제 구현과 비슷하게 동작하기 때문에 다른 테스트 대역들보다 우선적으로 활용됩니다. 대상 시스템은 자신이 이용하는 것이 실제 구현인지 가짜 객체인지 구분할 수 없어야 합니다.
public class FakeFileSystem implements FileSystem {
...
}
13.6.1 가짜 객체가 중요한 이유
가짜 객체는 빠를 뿐만 아니라 실제 객체를 사용할 때의 단점을 제거한 채 테스트를 효과적으로 수행할 수 있게 해줍니다. 가짜 객체를 거의 활용하지 않는 조직에서는 느리고 불규칙한 테스트를 낳아 엔지니어링 속도가 느려질 것입니다. 물론 스텁과 목과 같은 테스트 대역을 활용할 수도 있지만 이 둘은 테스트를 불명확하고 깨지기 쉽고 효과도 떨어지게 만들 가능성이 큽니다.
13.6.2 가짜 객체를 작성해야 할 때
가짜 객체는 만드는 노력도 더 들고 도메인 지식도 더 필요합니다. 실제 객체의 행위가 변경될 때마다 발맞춰서 갱신해야 하므로 유지보수도 신경써야 합니다.
유지보수할 가짜 객체 수를 줄이려면 우선 테스트에서 진짜 객체를 사용하지 못하게 만드는 근본 원인을 찾습니다. 그런 다음 해당 코드만 가짜 객체로 만드세요.
13.6.3 가짜 객체의 충실성
가짜 객체를 활용하는 핵심 이유는 “충실성”에 있을 것입니다. 여기서 충실성이란 가짜 객체가 실제 구현의 행위를 얼마나 비슷하게 흉내 내느냐를 말합니다.
100% 충실하게 만들기는 어렵지만 실제 구현의 API 명세에 가능한 한 충실해야 합니다. 실제 구현과 동일한 결과를 돌려주고 상태 변화도 똑같이 시뮬레이션해야 합니다. 예를 들어 database.save(itemId)라는 API가 있습니다. 실제 구현에서 주어진 id가 없다면 저장에 성공하고, 이미 존재한다면 에러를 내야 한다면 가짜 객체도 각 상황에서 똑같이 동작해야 합니다.
가짜 객체는 실제 구현에 완벽히 충실해야 하지만 해당 테스트의 관점에서만 그렇게 해주면 충분합니다. 또한 대부분의 테스트에서 관심 없는 행위라면 빠르게 실패해주는 게 가짜 객체의 미덕입니다.
13.6.4 가짜 객체도 테스트해야
초기에는 올바르게 작동하던 가짜 객체라도 세월이 흘러 실제 구현이 변경되면 실제 작동과 달라지게 됩니다. 자체 테스트로 이런 사태를 막아줘야 합니다.
이때 실제 구현과 가짜 객체 둘 다를 대상으로 하는 공개 인터페이스 검증 테스트를 작성하면 좋습니다.
[ 13.7 뭉개기(스텁) ]
13.7.1 스텁 과용의 위험성
- 불명확해진다
- 대상 함수에 행위를 덧씌우는 추가 코드는 읽는 이의 눈을 어지럽혀서 테스트 의도를 파악하기 어렵게 합니다.
- 만약 특정 함수를 스텁한 이유를 이해하기 위해 실제 시스템의 코드를 살펴본다면 스텁이 적합하지 않다는 결정적인 신호임
- 깨지기 쉬워진다
- 스텁을 이용하면 대상 시스템의 내부 구현 방식이 테스트에 드러납니다.
- 이는 제품의 내부가 변경되면 테스트 코드도 함께 수정해야 한다는 뜻이죠.
- 좋은 테스트라면 사용자에게 영향을 주는 공개 API가 아닌 한, 내부의 변경으로 인해 영향받지 않아야 합니다.
- 테스트 효과가 감소한다
- 실제 구현의 명세에 의존하는 시스템이라면 스텁을 사용하지 않는 게 좋습니다.
- 스텁으로 원래 행위를 뭉개버리면 해당 함수가 실제 구현과 똑같이 동작하는지 보장할 방법이 사라집니다.
- 또한 스텁을 이용하면 상태를 저장할 방법이 사라져서 대상 시스템의 특성 일부를 테스트하기 어려울 수 있습니다. 예를 들어 database.save(item)으로 item.id를 세팅할 수 없습니다.
13.7.2 스텁이 적합한 경우
스텁은 실제 구현을 포괄적으로 대체하기보다는 특정 함수가 특정 값을 반환하도록 하여 대상 시스템을 원하는 상태로 변경하려 할 때 제격입니다. 실제 구현이나 가짜 객체로는 원하는 반환값을 얻거나 특정 오류를 일으키기가 불가능할 수 있는데, 스텁으로는 쉽게 원하는 결과를 얻을 수 있습니다.
목적이 분명하게 드러나게 하려면 스텁된 함수 하나하나가 단정문들과 직접적인 연관이 있어야 합니다. 그래서 테스트들은 대체로 적은 수의 함수만 스텁으로 대체합니다. 스텁된 함수가 많을수록 테스트의 의도는 희미해질 것입니다. 스텀이 많이 눈에 띄는 것만으로도 스텁을 과용했따는 신호일 수 있습니다. 혹은 대상 시스템이 너무 복잡하니 리팩터링하라는 신호일 수도 있습니다.
스텁을 활용하기 괜찮은 상황일지라도 되도록 실제 구현이나 가짜 개게를 이용하라고 권하겠습니다. 이 둘은 시시콜콜한 구현 방식까지 노출하지 않으며, 코드가 훨씬 간결해지기 때문에 테스트 자체에 오류가 숨어들 가능성이 적습니다. 하지만 테스트가 지나치게 복잡해지지 않을 정도로 제한적으로만 사용한다면 스텁도 충분히 활용할 수 있는 기술입니다.
[ 13.8 상호작용 테스트하기 ]
상호작용 테스트는 대상 함수의 구현을 “호출하지 않으면서” 그 함수가 어떻게 호출되는지를 검증하는 기법입니다. 모의 객체 프레임워크를 활용하면 상호작용 테스트를 어렵지 않게 숳애할 수 있찌만 테스트를 가치 있고, 잘 읽히고, 변경하기 쉽게끔 관리하려면 꼭 필요할 때만 적용하는 것이 좋습니다.
13.8.1 상호작용 테스트보다 상태 테스트를 우선하자
상호작용 테스트보다는 되도록 상태 테스트를 이용하는 게 좋습니다.
상태 테스트(state test)란 대상 시스템을 호출하여 올바른 값을 반환하는지, 혹은 대상 시스템의 상태가 올바르게 변경되었는지를 검증하는 테스트를 말합니다.
구글은 오랜 경험을 통해 상태 테스트에 집중해야 훗날 제품과 테스트를 확장할 때 훨씬 유리하다는 사실을 깨달았습니다. 깨지기 쉬운 테스트가 줄어들고 나중에 테스트를 변경하거나 유지보수하기가 쉬워집니다.
상호작용 테스트의 가장 큰 문제는 대상 시스템이 특정 함수의 호출 여부만 알려줄 뿐, 올바르게 작동하는지는 말해주지 못한다는 점입니다. 그래서 해당 코드가 올바르게 동작한다고 “가정하고 넘어가야” 하죠. 이와 달리 상태 테스트는 가정이 실제로 이루어졌는지 검증해주므로 더 유익합니다.
두 번째 문제는 대상 시스템의 상세 구현 방식을 활용한다는 점입니다. 다시 말해 특정 함수가 호출되는지 검증하려면 대상 시스템이 그 함수를 호출할 것임을 테스트가 알아야 합니다. 스텁에서도 문제가 되었듯이 제품 코드의 구현 방시이 바뀌면 테스트가 깨질 수 있습니다.
13.8.2 상호작용 테스트가 적합한 경우
- 실제 구현이나 가짜 객체를 이용할 수 없어서(실제 구현은 너무 느리고 가짜 객체는 존재하지 않아서) 상태 테스트가 불가능한 경우. 이상적인 건 아니지만 대상 시스템이 기대한 대로 동작한다는 확신을 어느 정도는 얻을 수 있습니다.
- 함수 호출 횟수나 호출 순서가 달라지면 기대와 다르게 동작하는 경우. 상태 테스트로는 검증하기 어려운 상황이므로 상호작용 테스트가 제 역할을 할 수 있습니다.
상호작용 테스트는 상태 테스트를 완전히 대체하지 못합니다. 따라서 단위 테스트에서 상태 테스트를 수행할 수 없다면 상호작용 테스트를 추가하는 대신 더 큰 범위의 테스트 스위트에서 상태 테스트를 수행하여 보완하는 게 좋습니다.
13.8.3 상호작용 테스트 모범 사례
- 상태 변경 함수일 경우에만 상호작용 테스트를 우선 고려하자
- 상태 변경: 함수가 대상 시스템 바깥 세상에 부수효과를 남김 ex) sendMail, saveRecord
- 상태 유지: 부수 효과가 없으며, 시스템 바깥에 대한 정보를 반환함 ex) getUser, findResults()
- 너무 상세한 테스트는 피하자
- 어떤 함수들이 어떤 인수들을 받아 호출되는지를 너무 세세하게 검증하지 않아야 테스트가 더 명확하고 간결해짐
- 나아가 각 테스트의 범위를 벗어난 행위로 인한 변경의 영향을 덜 받으므로 함수 호출 방식이 달라진다고 실패하는 테스트의 수도 줄어듬
14. 더 큰 테스트
[ 14.1 더 큰 테스트란? ]
다음은 더 큰 테스트들의 특성입니다.
- 느릴 수 있습니다. 구글에서 대규모 테스트의 기본 타임아웃 값은 15분이나 1시간입니다. 심지어 몇 시간이나 며칠이 걸리는 테스트도 만들어 활용합니다.
- 밀폐되지 않을 수 있습니다. 대규모 테스트는 다른 테스트나 최종 사용자와 자원 및 트래픽을 공유하기도 합니다.
- 비결정적일 수 있습니다. 예컨대 밀폐되지 않은 대규모 테스트라면 다른 테스트나 사용자 상태에 영향을 받을 수 있어서 완벽히 결정적이라고 보장하기가 거의 불가능합니다.
단위 테스트는 개별 함수, 객체, 모듈에 대한 확신을 심어줍니다. 반면 더 큰 테스트들은 시스템 “전체”가 의도대로 동작한다는 확신을 더해주는 역할을 합니다. 또한 이들을 자동화해두면 (수동 테스트와 달리) 다양하게 확장할 수 있습니다.
14.1.1 충실성
더 큰 테스트가 존재하는 첫 번째 이유는 바로 충실성을 높이기 위함입니다. 충실성(fidelity)이란 테스트가 대상의 실제 행위를 얼마나 충실하게 반영했느냐를 나타내는 속성입니다.
단위 테스트가 대상 코드를 검증해주는 건 분명하지만 프로덕션 환경에서와는 매우 다르게 동작할 것입니다. 오른쪽 끝의 프로덕션은 당연히 테스트 충실성이 가장 높은 환경입니다. 이 양극단 사이에는 다양한 선택지가 존재합니다. 그리고 더 큰 테스트의 핵심은 이 사이에서 가장 적합한 지점을 찾아내는 것입니다. 충실성이 높아질수록 비용이 커져서 테스트 실패 시 입는 손해도 크기 때문입니다.
많은 경우 테스트 데이터가 현실과 동떨어져서 공들여 만든 수많은 대규모 테스트가 엔지니어들에게 외면받곤 합니다. 프로덕션 환경에서 수집한 데이터라면 실제를 훨씬 잘 반영할 것입니다. 하지만 제품을 론칭하기 전에는 현실적인 테스트 트래픽을 만들어내기가 쉽지 않습니다. 단위 테스트용 데이터는 대부분 수작업으로 만들어집니다. 그래서 다루는 사례가 몇 안되고 제작자의 편견이 반영되기 쉽습니다. 이처럼 데이터에서 누락되어 다루지 못한 시나리오가 테스트의 충실성 하락으로 이어집니다.
14.1.2 단위 테스트가 손 대기 어려운 영역
단위 테스트로는 위험 요인을 충분히 해소하기 어려운 대표적인 영역 다섯 가지를 짚어보겠습니다.
- 부정확한 테스트 대역
- 의존하는 대상이 존재하는 경우 모의 객체를 만들고 의도된 행위를 정의해야 할 수 있음
- 이때 의존 대상이 직접 관리하지 않는 코드라면, 실제 동작을 잘못 이해하고 테스트를 작성하여 테스트가 부정확해질 수 있음
- 더욱이 모의 객체는 실제 구현을 사용하지 않으므로, 실제 구현이 수정될 때 테스트와 테스트 대상 코드도 함께 수정되어야 한다는 신호를 주지 못할 수 있음
- 설정 문제
- 일반적으로 실행 파일은 배포 설정이나 시작 스크립트 같은 게 필요한 경우가 많음
- 설정 파일에 문제가 있거나 데이터베이스에 정의된 상태가 다르게 테스트한 후 프로덕션에 배포하면 사용자에게 심각한 문제를 일으킬 수 있음
- 하지만 단위 테스트 만으로는 이러한 호환성 문제를 검증할 수 없음(설정도 코드처럼 버전 관리가 되어야 함)
- 구글의 경우 심각한 서비스 장애를 일으키는 가장 주된 원인이 바로 설정 변경임
- 과부하 시 나타나는 문제
- 성능, 부하, 스트레스 테스트는 통상적인 단위 테스트 모델에 녹이기 어려움
- 예기치 못한 동작, 입력, 부작용
- 단위 테스트의 범위는 작성자가 예상할 수 있는 행위와 입력에 한해서 테스트되기 쉬움
- 예상치 못한 행위를 테스트하기 위해서는 다른 기술이 필요할 수 있음
- 하이럼의 법칙: 우리가 약속된 모든 기능을 100% 테스트하더라도, 실제 사용자들은 명시된 약속뿐 아니라 눈에 보이는 모든 것을 자유롭게 이용해볼 수 있음
- 단위 테스트만으로는 공개 API에 명시되지 않은 행위까지 모두 확인할 가능성이 극히 낮음
- 창발적 행위와 “진공 효과”
- 단위 테스트는 빠르고 안정적이게끔 설계하므로 실제 의존하는 대상 혹은 현실 세계의 네트워크와 데이터에 연결했을 때 발생할 수 있는 혼돈은 의도적으로 배제함
- 단위 테스트는 혼돈으로 가득한 현실과는 동떨어진 “진공 상태를 가정”하고 문제를 푸는 것임
- 그러면 속도와 안정성은 매우 뛰어나지만 특정 범주의 결함들은 놓치기 쉬움
14.1.3 더 큰 테스트를 만들지 않는 이유
좋은 단위 테스트라면 다음의 특징을 모두 갖춰야 하지만, 더 큰 테스트에서는 하나도 갖추지 못하는 경우도 생깁니다.
- 높은 신뢰성: 결과가 불규칙하면 안 되며, 유용한 성공/실패 신호를 제공해야 합니다.
- 빠른 속도: 개발자 워크플로를 방해하지 않을 정도로 빨라야 합니다.
- 높은 확장성: 구글은 변경되는 코드에 영향을 받는 모든 테스트를 서브밋 직전과 직후에 효율적으로 실행할 수 있어야 합니다.
또한 더 큰 테스트가 극복해야 할 과제가 두 가지 더 있습니다.
- 소유권 문제
- 단위 테스트는 누가 소유자인지 명확하지만 더 큰 테스트는 다수의 단위에 걸쳐 있으므로 관련된 소유자 역시 많으며, 시간이 흐를수록 소유권이 모호해짐
- 유지보수는 누가 책임지고, 테스트가 실패하면 누가 문제를 진단할 것인가? 소유권이 명확하지 않다면 테스트는 서서히 부패할 것임
- 표준화 혹은 표준화 부족
- 더 큰 테스트는 작성하고 실행하고 디버깅하기 위한 인프라와 프로세스가 부실하며, 테스트 수행 방식은 시스템 아키텍처에 따라 달라지므로 테스트 유형이 다양함
- 이 모든 테스트를 수행하는 표준화된 방식이 없으므로 자연스럽게 인프라의 지원을 받지 못함
[ 14.2 더 큰 테스트 @ 구글 ]
14.2.1 더 큰 테스트와 수명
단위 테스트는 기대 수명이 몇 시간 이상만 되면 충분히 가치가 있습니다.
개발 초기에 코드가 몇 분 정도만 쓰이고 사라질 거라 판단하여 수동 테스트에 의존하면 수동 테스트들이 누적되어 초기 테스트 포트폴리오 전체를 지배하게 됩니다.
건강한 상태를 오래 유지하는 “핵심”은 개발 시작 후 며칠 안으로 단위 테스트를 만들어 테스트 피라미드를 쌓기 시작하는 것입니다. 그런 다음 수동으로 수행하던 종단간 테스트를 자동화된 통합 테스트로 대체해 피라미드 위층으로 올립니다. 구글은 코드를 서브밋하려면 “반드시” 단위 테스트를 포함하도록 규정하여 해결했습니다. 오랫동안 코드를 건강하게 유지하려면 단위 테스트와 수동 테스트 사이의 간극을 매우는 데 소흘해서는 안 됩니다.
14.2.2 구글 규모에서의 더 큰 테스트
규모가 큰 소프트웨어라면 더 큰 테스트가 그만큼 더 필요하고 유용합니다. 하지만 아쉽게도 작성하고 수행하고 관리하고 디버깅하는 복잡도는 규모가 커질수록 함께 증가합니다.
종단간 테스트가 필요한 개별 시나리오의 수는 테스트 대상(SUT)의 구조에 따라 기하급수적으로 혹은 조합의 수만큼 늘어나므로 이런 식의 성장은 확장하는 데 한계가 분명합니다. 따라서 시스템의 성장에 맞춰 더 큰 테스트들도 지속해서 관리할 수 있으려면 새로운 전략을 모색해야 합니다.
하지만 서비스 규모를 이만큼 기우기 위해 구성요소의 수를 늘려놨기 때문에 더 큰 테스트들을 통해 얻는 가치 역시 커집니다. 그리고 이 가치에는 충실성이 큰 영향을 줍니다.
따라서 더 큰 테스트들은 원하는 규모에서 잘 동작하면서도 충실성이 상당히 높게 구현하는 게 관건입니다.
=== 가능한 한 작은 테스트 ===
통합 테스트라 하더라도 가능한 한 작을수록 좋습니다. 초거대 테스트 하나보다 거대한 테스트 여러 개가 낫다는 말입니다. 그리고 테스트 범위는 SUT의 범위와 관련이 높기 때문에 SUT를 더 작게 만드는 방법을 찾으면 테스트를 더 작게 만드는 데 유리합니다.
여러 개의 내부 시스템이 관여하는 기능을 테스트하면서 테스트 크기를 작게 만드는 좋은 전략으로 “연쇄 테스트(chain test)”라는 방법이 있습니다. 기능 전체를 아우르기보다는 작은 통합테스트듣ㄹ로 나눠 연결하는 것입니다. 이때 앞 단계의 테스트 결과를 리포지터리에 저장한 다음, 그다음 단계 테스트의 입력으로 사용합니다.
[ 14.3 테스트의 구조 ]
다음은 큰 테스트를 진행하는 일반적인 흐름입니다.
1. 테스트 대상 시스템 확보
다양한 형태의 테스트 대상 시스템(SUT, System Under test)가 존재하며, 밀폐성과 충실성에 의해 테스트 범위가 결정됨. 이 둘은 충돌할 때가 많음
- 밀폐성
- 현재 테스트하려는 기능과 관련 없는 구성요소를 사용하거나 사용하지 못해야 함
- 밀폐성이 높으면 동시성 문제나 불규칙한 인프라로부터 영향을 적게 받음
- 충실성
- 충실성이 높은 SUT는 프로덕션 버전과 유사한 바이너리로 구성됨
- 충실성이 높으면 테스트 신뢰성을 떨어뜨리고 피드백 시간을 늘리는 주범이 될 수 있음
핵심은 충실성과 비용/신뢰성 사이에서의 균형점과 합리적인 경계를 찾는 것입니다.
구글 외부에서는 고객 주도 계약(consumer-driven contract) 테스트용 프레임워크를 활용하는 사례가 늘고 있습니다. 고객과 서비스 제공자 모두가 지켜야 할 계약(명세)을 정의하고, 이 계약을 토대로 자동화된 테스트를 만들어내는 방식입니다. 구체적으로, 고객이 서비스의 모의 객체를 정의하며 이때 어떤 입력을 주면 어떤 결과를 받게 되는지를 명시합니다. 그런 다음 실제 서비스는 이 입력/결과 쌍을 실제 테스트에 활용하여 기대한 결과를 반환하는지를 검증합니다. 대표적으로 Pact Contract Testing과 Spring Cloud Contracts가 유명합니다.
구글이 가장 많이 쓰는 방식은 더 큰 테스트를 실행해 외부 서비스들과의 트래픽을 기록해뒀다가, 이 트래픽을 더 작은 테스트를 수행할 때 재생하는 것입니다. 여기서 흥미로운 점은 비결정성을 없애기 위해 매칭기(matcher)를 이용하여 요청을 보고 기대하는 응답과 연결시킨다는 것입니다. 스텁이나 모의 객체를 사용할 때 인수를 보고 결과 행위를 결정하는 방식과 매우 비슷합니다.
2. 테스트 데이터
- 시드 데이터: 테스트 개시 시점의 SUT 상태를 반영하여 SUT를 사전 초기화해주는 데이터
- 테스트 트래픽: 테스트 수행 과정에서 SUT로 보내는 데이터
- 도메인 데이터: 환경 구성용 데이터가 테이블들에 미리 채워져 있어야 함, DB를 활용한다면 적절한 도메인 데이터가 필요함
현실적인 SUT가 되려면 품질과 양적 측면 모두에서 현실적인 데이터셋이 기본으로 갖춰져 있어야 합니다.
3. 검증
- 수동 검증: 사람이 SUT와 직접 상호작용 하며 올바르게 동작하는지 확인함
- 단정문: 시스템이 의도된 대로 동작하는지 명확히 검증함
- A/B 비교: 두 벌의 SUT를 구동시켜 똑같은 데이터를 보낸 다음 결과를 비교함
[ 14.4 더 큰 테스트 유형 ]
- 하나 이상의 바이너리에 대한 기능 테스트
- 대표적으로 서비스 각각을 독립적인 바이너리로 배포하는 마이크로서비스 환경이 있음
- 연관된 바이너리를 모두 포함한 SUT를 구동시키고 오픈 API를 통해 바이너리 사이의 실제 상호작용 검증
- 브라우저와 기기 테스트
- 성능, 부하, 스트레스 테스트
- 배포 설정 테스트
- 데이터 파일, DB, 옵션 등의 설정 파일에 대한 테스트
- 추가 데이터나 검증이 많이 필요 없는 스모크 테스트에 해당함, 제대로 구동하면 통과이고 그렇지 않으면 실패
- 탐색적 테스팅
- 새로운 사용자 시나리오를 시도해가며 의문스러운 동작을 찾는 수동 테스트
- 새로운 실행 경로로 시험해보며 예상이나 직관과 다르게 동작하는지 혹은 보안 취약점은 없는지 찾음
- 이미 서비스 중인 시스템에서도 예상치 못한 동작과 부작용을 발견할 수 있어 유용함
- 모든 결함은 자동 테스트로 만들어서 다음번에는 사람이 수행하지 않도록 해야 함
- A/B 차이(회귀) 테스트
- 구버전과 신버전의 API로 트래픽을 보내 반응이 어떻게 다른지 비교하는 방식(2벌의 SUT 필요)
- 모든 차이는 기대한 반응과 기대하지 않은 반응(회귀)으로 구분함
- 사용자 인수 테스트(UAT)
- 단위 테스트는 “의도한 대로 동작하는가”가 아니라 “구현한 대로 동작하는가”를 확인할 분임
- 따라서 특정한 최종 고객 혹은 대리인이 있는 경우, 사용자 인수 테스트는 특정 사용자의 여정(user journey)이 의도한 대로 이루어지는지를 보장하는 테스트임
- 사실 구글은 자동화된 UAT를 잘 활용하지 않는데, 역사적으로 자신들이 필요해서 만들었기 때문임. 소스 코드를 읽는 데 막힘이 없는 사람들이 고객이므로, 실행 가능한 명세 언어가 필요 없었음
- 프로버와 카라니 분석
- 프로버
- 프로덕션 환경을 대상으로 단정문을 수행하는 기능 테스트
- 대체로 시간이 흘러 프로덕션 데이터가 변경되어도 단정문이 지켜지는, 즉 잘 알려지고 결정적인 읽기 전용 동작이 검증 대상임
- 카나리
- 프로덕션 서비스 중 일부를 새로운 버전으로 조금씩 대체해가면서 신버전과 기존 버전 모두를 대상으로 프로버를 수행함
- 프로버
- 재해 복구와 카오스 엔지니어링
- 사용자 평가
[ 14.5 큰 테스트와 개발자 워크플로 ]
14.5.1 큰 테스트 작성하기
구글은 단위 테스트에서 쓰는 단정문 라이버리를 기능 통합 테스트에서도 씁니다. 하지만 SUT와의 상호작용, A/B 테스트, 테스트용 시드 데이터 생성, 테스트 워크플로 조직 등에 필요한 라이브러리들도 시간을 들여 따로 구축해왔습니다.
더 큰 테스트를 관리하려면 투입되는 시간과 자원 모든 면에서 비용이 많이 듭니다.
14.5.2 큰 테스트 수행하기
- 테스트 속도 개선하기
- 테스트가 느릴수록 엔지니어가 수행하는 빈도가 줄어들음
- 테스트의 속도를 높이기 위해 테스트 범위를 줄이거나 병렬로 수행할 수 있음
- 그 외에도 내부 시스템 타임아웃과 지연 낮추기 & 테스트 빌드 시간 최적화 등도 가능함
- 불규칙한 결과에서 벗어나기
- 테스트 범위를 줄여야 하며, 밀폐된 SUT라면 실제 불규칙성 문제를 걱정하지 않아도 됨
- 때에 따라서는 테스트 속도와 불규칙한 결과 사이에서 절충점을 찾아야 함
- 이해되는 테스트 만들기
- 다른 사람이 변경한 코드가 내 테스트를 실패하게 만들었는데, 그 사람의 코딩 방식이 익숙치 않다면 실패 이유를 이해하기가 쉽지 않음
- 따라서 무엇이 실패했는지 명확히 알려주고, 최소한의 노력으로 근본 원인을 찾을 수 있게 하고, 지우언 정보 및 연락처 정보를 제공할 수 있음
15. 폐기
[ 15.1 폐기시키는 이유 ]
이번 논의는 “코드는 자산이 아니라 부채다” 라는 기본 전제에서 시작합니다. 코드에는 비용이 따라옵니다. 폐기는 시대에 뒤처졌음을 보여줄 수 있고 비슷한 기능의 대체재가 존재하는 시스템에 적합합니다.
=====
코드 “자체”는 가치를 창출하지 않습니다. 가치를 만들어내는 건 바로 “기능”입니다. 사용자의 요구에 부합하는 기능은 자산입니다. 이 기능을 구현하는 코드는 그저 목적지로 가기 위한 수단인 것이죠.
똑같은 기능을 하는 두 코드가 있습니다. 하나는 이해하기 쉽게 짜여서 유지보수도 문제가 없는 한 줄 짜리이고, 다른 하나는 1만 줄이나 되는 난해한 스파게티 코드입니다. 우리는 전자를 선호합니다. 코드 자체는 비용을 낳기 때문에 기능이 같다면 코드 자체는 단순할수록 좋습니다.
따라서 우리는 얼마나 많은 코드를 작성하느냐나 코드베이스가 얼마나 큰가가 아니라, 단위 코드당 얼마나 많은 기능을 제공하느냐에 집중하여 이 지표를 극대화해야 합니다. 더 많은 기능을 제공하길 기대하면서 코드의 양만 늘려서는 안 됩니다. 오히려 지나친 코드나 더 이상 필요 없는 시스템들을 제거해야 하죠. 이때 필요한 것이 바로 폐기 정책과 절차입니다.
16. 버전 관리와 브랜치 관리
[ 16.3 버전 관리 @구글 ]
구글의 소스 코드 대부분은 모노리포에서 관리되며, 약 5만여 엔지니어에게 공유됩니다. 크로미움과 안드로이드 같은 오픈 소스 프로젝트를 제외하고는 구글이 주관하는 프로젝트 거의 모두가 이 안에서 이루어집니다.
구글이 선택한 버전 관리 정책에 맞춰 시스템을 수정할 수 있다는 점은 자체 개발한 제품의 커다란 장점입니다. 예를 들어 우리는 소유권을 세분화해 관리합니다. 그래서 모든 디렉터리 계층에 OWNERS 파일을 두어 하위 디렉토리로의 커밋을 승인할 수 있는 엔지니어를 기록해둡니다.
16.3.5 버전 관리의 미래
모노리포에 대한 가장 큰 우려는 모든 것을 하나의 리포지터리에 담을 수 있는 기술이 있느냐입니다.
'나의 공부방' 카테고리의 다른 글
[Tool] 유용한 MacOS 앱 및 크롬 익스텐션(Chrome Extensions) 정리 (7) | 2024.10.29 |
---|---|
[개발서적] 단위 테스트(Unit Testing) 핵심 내용 정리 및 요약 (16) | 2024.10.15 |
[Architecture] 헥사고날 아키텍처를 통한 의미 수준과 구현 수준에 대한 이해(semantic and implementation level with hexagonal architecture) (24) | 2024.09.24 |
[개발서적] 스트리트 코더(Street Coder) 핵심 내용 정리 및 요약 (10) | 2024.08.27 |
[개발서적] 오브젝트 핵심 내용 정리 및 요약 (2) | 2024.08.20 |