Self-testing Code
정의
Self-testing Code는 명령 하나로 테스트 전체를 실행할 수 있고, 그 테스트가 통과하면 코드에 중대한 결함이 없다고 확신할 수 있는 상태를 가리킨다. Martin Fowler가 Refactoring에서 이 실천에 붙인 이름이다.
Fowler가 제시하는 사고 모형은 명확하다. 소프트웨어 시스템을 만드는 동시에 그 시스템 내부의 결함을 감지하는 버그 탐지기를 함께 만든다. 팀의 누군가가 실수로 버그를 넣으면 탐지기가 울린다. 스위트를 하루에도 여러 번 돌리면 버그가 들어온 직후에 감지되므로 최근 변경분만 살펴보면 되고, 그만큼 찾기가 쉬워진다.
여기서 나오는 태도가 하나 있다. 테스트가 없는 사소하지 않은 코드는 깨져 있다고 가정한다. 작업 에피소드는 동작하는 코드와 그것을 계속 동작하게 유지할 테스트가 모두 있어야 끝난 것으로 본다.
유래는 Fowler가 OOPSLA 컨퍼런스에서 Dave Thomas의 “모든 객체는 스스로를 테스트할 수 있어야 한다"는 말을 들은 데서 시작한다. 이후 Kent Beck이 같은 일을 훨씬 정교한 방식으로 하고 있다는 것을 알게 됐고, 얼마 뒤 Beck과 Erich Gamma가 JUnit을 내놓으면서 이 사고와 실천의 기반이 됐다.
TDD와 같은 것이 아니다
이 문서에서 가장 중요한 구분이다. TDD와 Self-testing Code는 다른 개념이다.
TDD는 self-testing code를 만들어내는 것이 이점 중 하나인 특정한 실천법이다. 좋은 방법이긴 하지만, 코드를 먼저 쓰고 테스트를 나중에 써서도 self-testing code를 만들 수 있다. 중요한 것은 테스트가 있다는 사실이지 거기에 어떻게 도달했는가가 아니다.
단, 테스트가 있고 통과하기 전까지는 작업이 끝난 것으로 치지 않는다는 조건이 붙는다.
(테스트 우선)"] --> S["Self-testing Code
(결과 상태)"] A["Test-after
(테스트 후행)"] --> S S --> R["안전한 리팩터링"] S --> C["Continuous Integration"]
이 구분이 실무에서 중요한 이유는 두 가지가 뒤섞이면 논쟁이 성립하지 않기 때문이다. Is TDD Dead? 대담 1편에서 Fowler가 가장 먼저 정리한 것도 이 지점이다. DHH가 제기한 문제는 test-first라는 방법에 대한 것이었지 자동화된 회귀 테스트라는 결과에 대한 것이 아니었다. DHH 본인도 원문에서 TDD가 자동화된 회귀 테스트에 눈을 뜨게 해준 것에 감사한다고 적으면서, 버린 것은 설계 도그마로서의 test-first라고 밝혔다.
대담의 결론도 여기에 있다. 세 사람 모두 self-testing code에 가치를 두었고, TDD가 어떤 맥락에서는 작동한다는 데 동의했으며, 적용 가능성이 맥락에 달렸다는 점을 인정했다.
진짜 이득은 버그 감소가 아니다
Self-testing code의 눈에 띄는 이득은 프로덕션에 들어가는 버그 수를 크게 줄인다는 것이다. 그런데 Fowler가 가장 큰 이득으로 꼽는 것은 따로 있다. 시스템을 변경할 수 있다는 자신감이다.
테스트가 없는 오래된 코드베이스는 개발자가 동작하는 코드를 건드리기 두려워하는 곳이 된다. 버그 하나를 고치는 일조차 위험한데, 고치는 것보다 더 많은 버그를 만들 수 있기 때문이다. 그 결과는 악순환이다.
Self-testing code가 있으면 그림이 뒤집힌다. 코드를 깨끗하게 만드는 작은 수정을 안전하게 할 수 있다고 믿게 되는데, 실수하면 탐지기가 울려서 빠르게 되돌리고 계속 진행할 수 있기 때문이다. 그 안전망 위에서 코드를 좋은 상태로 유지하는 데 시간을 쓸 수 있고, 결과적으로 새 기능을 추가하는 속도가 꾸준히 빨라지는 선순환에 들어간다.
리팩터링과의 관계
Fowler의 리팩터링 정의는 두 가지다.
리팩터링(명사): 소프트웨어를 이해하기 쉽고 수정 비용이 싸게 만들기 위해 관찰 가능한 동작을 바꾸지 않으면서 내부 구조에 가하는 변경
리팩터링(동사): 관찰 가능한 동작을 바꾸지 않으면서 일련의 리팩터링을 적용해 소프트웨어를 재구조화하는 것
두 정의 모두 “관찰 가능한 동작을 바꾸지 않으면서"라는 조건을 달고 있다. 그 조건이 지켜졌는지 확인할 수단이 없으면 리팩터링은 성립하지 않는다. 구조를 바꿔 놓고 동작이 같기를 바라는 것은 리팩터링이 아니라 추측이다.
그래서 self-testing code는 리팩터링의 부산물이 아니라 전제 조건이다. Red-Green-Refactor에서 Refactor 단계가 성립하는 근거도 앞선 두 단계에서 테스트가 이미 확보되어 있다는 점이다.
Continuous Integration의 기둥
Fowler는 더 강하게 말한다. self-testing code가 없으면 Continuous Integration을 하고 있는 것이 아니다. CI의 핵심은 통합된 결과가 여전히 동작하는지 자동으로 확인하는 것인데, 그 확인 수단이 곧 테스트 스위트이기 때문이다. CI의 기둥인 만큼 Continuous Delivery에도 필수 요소가 된다.
실행 단위를 어떻게 나눌지는 테스트 피라미드와 단위의 범위에서 다룬 compile suite / commit suite 분리가 그대로 적용된다.
프로덕션 버그에 대한 반응
Self-testing code를 실천하는 팀의 특징적인 행동이 하나 있다. 프로덕션 버그가 발견됐을 때 먼저 그 버그를 드러내는 테스트를 쓰고, 그다음에 고친다.
실제로는 하나의 테스트가 아니라 범위를 점점 좁혀 가는 일련의 테스트가 되는 경우가 많다. 넓은 범위에서 시작해 버그를 유발하는 단위 테스트에 도달할 때까지 좁힌다. 이 과정 자체가 유용한 디버깅 기법이고, 동시에 버그가 고쳐진 뒤 되살아나지 않게 보장하는 수단이다.
여기서 한 걸음 더 나간다. 팀은 그 버그를 비슷한 테스트가 또 누락되지 않았는지 찾아보는 계기로 삼는다. 태도의 핵심은 이것이다.
버그는 코드의 실패일 뿐 아니라 똑같이 테스트의 실패다.
이 원칙은 테스트 피라미드와 단위의 범위에서 정리한 “고수준 테스트에서 실패가 나면 단위 테스트도 누락된 것"이라는 규칙과 같은 뿌리다.
프로덕션 모니터링이라는 차원
최근에는 self-testing에 또 하나의 축이 더해지고 있다. 프로덕션 모니터링이다.
Continuous Delivery가 새 버전을 빠르게 배포할 수 있게 만들면서, 팀은 프로덕션에 올라간 뒤 버그를 포착하고 신속히 대응하는 쪽에 더 많은 노력을 들이게 됐다. 대응 방식은 수정 버전을 새로 배포하거나 마지막으로 정상 동작하던 버전으로 롤백하는 것이다.
이 방향은 배포 전 테스트를 대체하는 것이 아니라 보완한다. Is TDD Dead? 3편에서 Kent Beck이 든 사례도 같은 맥락이다. 온콜 로테이션은 테스트되지 않은 시나리오에 대한 결정적인 피드백을 제공하며, 테스트가 초록색이라고 해서 프로덕션에서의 신뢰성이 보장되지는 않는다.
관련 문서
- TDD 개요 - TDD의 정의와 “TDD is dead” 논쟁
- 테스트 피라미드와 단위의 범위 - 테스트 종류의 배치와 스위트 분리
- Red-Green-Refactor - 안전망 위에서 이루어지는 리팩터링 단계
- 테스트 더블 - 스위트를 빠르게 유지하기 위한 대역
참고 자료
- Martin Fowler, Self Testing Code (2014)
- Martin Fowler, Definition of Refactoring (2004)
- Martin Fowler, Is TDD Dead? - 특히 1편(TDD and Confidence), 3편(Feedback and QA)
- Martin Fowler, Refactoring: Improving the Design of Existing Code (2nd ed., 2018)