유닛 테스트는 버그를 잡는다기보다 영역 표시를 하는 것
요약
본 글은 유닛 테스트가 버그를 잡는 것보다 '코드 영역 표시'라는 사회적 기능을 수행하며 보편화된 배경을 분석합니다. 소프트웨어 개발 환경에서 끊임없이 변화하는 코드베이스의 안정성을 확보하기 위해, 테스트 코드는 일종의 소유권 주장 및 변경 방지 메커니즘으로 작용한다는 것이 핵심입니다.
핵심 포인트
- 유닛 테스트는 버그 탐지보다 '코드 영역 표시'가 주된 목적이다.
- 소프트웨어 개발은 끊임없는 변화를 전제로 하며, 이 과정에서 안정성 확보가 중요하다.
- 테스트 코드는 코드의 소유권을 주장하고 무분별한 변경을 막는 사회적 장치 역할을 한다.
- 품질 개선은 프로그램이 빠르게 변경되는 속도(벨로시티)와 연결되어 가치를 인정받는다.
버그를 잡는 데 그다지 효과적이지 않은 유닛 테스트가 왜 업계에서 유일하게 보편화된 테스트 방법이 됐는지를 분석한 글
하드웨어 관점에서 소프트웨어 테스트는 끔찍해 보임 (저자는 하드웨어 개발 경험이 많은 프로그래머)
소프트웨어는 "나중에 고치면 된다"는 문화로 버그투성이 코드 출시가 허용되는 반면, 하드웨어 버그는 치명적이라 선택의 여지가 없음
유닛 테스트가 버그를 잘 못 잡는 두 가지 이유
까다로운 버그와 성능 문제의 상당수는 컴포넌트 간 상호작용에서 나오므로 여러 팀의 컴포넌트를 함께 돌리는 통합 테스트가 필요한데 통합 테스트는 매우 드묾
오히려 사람들은 자기 "유닛" 밖을 건드리지 않으려고 의존성 모킹에 무리한 일들을 함(모킹을 쉽게 하려고 추상 클래스만 쓰거나 테스트마다 API 스텁을 만들어 API 변경 시 호출부에 더해 스텁까지 전부 고쳐야 하게 만드는 식)
까다로운 버그를 찾으려면 아주 많은 입력 조합이 필요한데 유닛 테스트는 소수의 입력과 출력을 하드코딩하는 방식임
하드웨어 업계에서는 랜덤 입력 생성이 일반적이며 이는 입력 분포와 "출력이 올바른지 판별하는 방법"을 고민하게 만듦
반면 유닛 테스트의 기대 출력은 그냥 코드를 돌려 나온 값을 "정답"으로 하드코딩한 것이 전형적인 기원임
그런데 왜 유닛 테스트만 보편화됐는가
저자의 생각: 유닛 테스트의 주된 용도는 모든 것이 통제 없이 끊임없이 바뀌는 환경에서 남들이 내 코드를 함부로 망가뜨리지 못하게 막는 것임
코드 소유권이 없고("공유 소유권"이라는 완곡어법) 모두가 모든 것을 바꾸라고 독려받는 문화에서 "망가뜨림"은 사회적 구성물임. 고치도록 강제되어야만 "망가뜨린 것"이 됨
유닛 테스트가 완벽한 완화책인 이유: 쓰기 쉽고, 싸고 빨라서 커밋 게이트(lint, test 등 머지가 가능한지 테스트하는 것)로 걸어도 반발이 적고, 조직이 품질에 지불할 의향이 있는 딱 그만큼의 비용으로 "품질" 체크박스를 채워주며, 남의 테스트를 작성자와 상의도 없이 지우거나 억지로 통과하게 고치는 것은 너무 볼썽사나운 일이라 결국 테스트를 통과시키거나 작성자와 대화해야 하는 메커니즘이 생김
이것이 경영진과 프로그래머 모두 만족하는 균형점이며 테스트 없이 정말 빠르게 달리다가 서로가 낸 피해를 복구하느라 허둥대는 것보다는 실제로 더 빠르게 움직이게 해줌
테스트가 코드 크기를 대략 두 배로 만드는 이유도 여기서 나옴 — 같은 코드를 두 번 쓰는 셈이기 때문임. 첫 번째로 쓴 코드 본체는 공유 소유권 문화에서 누구나 마음대로 고칠 수 있지만, 테스트라는 형태로 한 번 더 써두면 남들이 함부로 고칠 수 없게 됨. "나 진심으로 이렇게 만든 거니까 마음대로 뜯어고치지 말아 달라"고 한 번 더 못박아두는 것과 같음
버그를 찾기 위한 테스트라면 코드보다 훨씬 길 수도 짧을 수도 있지만, 영역 표시가 목적이라면 지켜야 할 코드가 많을수록 표시도 많이 필요하므로("이 함수도 아직 잘 있고, 저 함수도 아직 잘 있고...") 테스트 양이 코드 양에 비례하게 됨
왜 2010년대인가: 브랜치와 병합이 쉬운 DVCS(git 등)가 주류가 된 시기와 겹침.
유닛 테스트로 가득한 CI 없이 전체 코드베이스의 병합이 반복되면 코드가 어떻게 될지 상상해보라는 것. CI와 DVCS는 거의 같은 시기에 보편화됐고 유닛 테스트의 인기도 그와 함께 폭발함
소프트웨어 품질의 일반 원리
하드웨어 개발자라면 "통합 테스트와 랜덤 테스트를 돌리는 전담 검증팀을 두면 되지 않냐"고 묻겠지만, 소프트웨어 쪽의 답은 이미 커밋하고 넘어간 코드에 버그를 접수하는 팀을 원치 않으며, 모든 것을 계속 바꾸는 게 목적인데 그 팀을 위해 API 안정성을 신경 써야 하고, 무엇보다 속도를 늦추기 때문에 절대 금지라는 것임
저자가 도출하는 일반 원리: 품질 개선은 프로그램이 변경되는 속도를 높여줄 때, 오직 그때만 가치 있는 것으로 인정받음
"벨로시티"를 희생해 품질을 높이려는 사람은 누구나 이 원리를 불쾌한 방식으로 체감하게 됨
이것이 소프트웨어의 테스트 방법이 하드웨어와 그토록 다른 이유이며 버그 박멸이 중요한 하드웨어의 효과적인 접근을 소프트웨어 팀이 거의 도입하지 않는 이유임
P.S. 랜덤 테스트 추천
까다로운 코드를 제대로 만들고 싶다면 랜덤 입력 생성기(randomizer)를 추천함
24시간 돌릴 필요 없이 수만 번 정도면 되고, 결정론적 시드를 쓰면 CI에 넣어 최소 그 입력 집합에 대해서는 남들이 코드를 못 망가뜨리게 할 수 있음(입력이 진짜 랜덤이면 CI가 비결정적이 되어 사람들이 통과할 때까지 재실행해버림)
저자는 수천~수만 줄 규모의 "모듈" 수준에서 랜덤 테스트를 함
계속 바뀌는 내부 API가 아니라 바꾸기 어려운 안정된 API를 대상으로 해야 그만한 투자가 가능함. 만드는 데 몇 주가 걸리지만 한번 만들면 하드웨어급 신뢰성에 근접함
요지: 진지한 통합 테스트는 조직이 원하지 않으면 개인이 할 수 없지만, 랜덤 테스트는 아무도 안 해도 혼자서 도입해 이득을 볼 수 있음. "DDT(Development-Driven Testing)"라는 후속 글을 예고함
P.P.S. 에이전트와 유닛 테스트
오늘날의 코딩 에이전트는 테스트를 잘하도록 강화학습된 것이 아니라 인간의 코드베이스에서 배웠기 때문에 꽤 어리석은 테스트로 코드 크기를 대략 두 배로 만듦
그래도 에이전트가 주로 편집하는 코드베이스에서는 없는 것보다 나음
에이전트는 코드를 전혀 모르는 채 방금 온보딩된 눈치 빠른 경력직 신입 같아서, 그런 존재가 대부분의 변경을 하는 곳에서는 코드가 금세 기능과 구조를 잃는데, 에이전트는 테스트 실패를 존중하도록 학습되어 있어 파괴적 변경에 대한 가드레일이 됨
다만 복잡하고 불필요한 구조가 추가되고 그것이 다시 테스트로 보호되어 미래의 리팩터링을 막는 것까지는 못 막음
AI 자동 생성 콘텐츠
본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기