테스트에 대해 우리는 대개 이렇게 생각한다. 테스트는 시스템을 바깥에서 찔러 보는 도구이며, 시스템 본체와는 별개의 무엇이다. 코드가 있고, 그 코드가 잘 도는지 확인하는 검사원이 따로 서 있는 그림이다. 27장까지 아키텍처 경계를 따라온 마틴은 이 장에서 그 그림을 뒤집는다. 테스트는 시스템 바깥에 있지 않다. 테스트는 시스템의 일부이며, 아키텍처의 관점에서 보면 모든 컴포넌트 중 가장 바깥쪽 원에 놓인 컴포넌트다. 그리고 가장 바깥 원인 이상, 테스트 역시 의존성 규칙을 따른다 — 소스 의존성은 언제나 안쪽을 향하고, 안쪽은 테스트의 존재조차 모른다.
이 한 번의 관점 전환이 이 장의 실천적 결론을 전부 끌고 나온다. 테스트를 시스템의 일부로 보는 순간, 잘 설계된 컴포넌트에 요구하던 것을 테스트에도 똑같이 요구하게 되기 때문이다.
테스트는 가장 바깥 원의 컴포넌트다
동심원을 다시 떠올려 보자(22장). 엔티티가 한복판에 있고, 유스케이스, 인터페이스 어댑터, 그리고 프레임워크와 드라이버가 바깥으로 갈수록 겹겹이 둘러싼다. 테스트는 이 그림 어디에 놓이는가. 마틴의 답은 명확하다 — 어느 원보다도 바깥이다. 테스트는 시스템의 무엇에든 의존할 수 있지만, 시스템의 어느 부분도 테스트에 의존해서는 안 된다.
이것은 테스트가 가진 가장 독립적인 성질을 정확히 짚는다. 테스트는 배포되지 않는다. 운영 환경에 올라가지도 않는다. 아무도 테스트에 의존하지 않으므로, 테스트는 시스템의 나머지에 아무 영향을 주지 않고 언제든 바뀔 수 있다. 다른 컴포넌트는 하나를 고치면 그것에 의존하던 것들이 함께 흔들리지만, 테스트는 아무리 뜯어고쳐도 바깥으로 파급이 없다. 의존성 그래프의 맨 끝, 아무도 가리키지 않는 잎(leaf) 노드 — 그것이 테스트의 자리다.
판단 기준: 어떤 코드가 “시스템의 일부인가”를 물을 때, 그것이 배포되는지가 아니라 의존성 규칙을 따르는지로 판단하라. 테스트는 배포되지 않아도 안쪽을 향해 의존하므로 엄연히 시스템의 일부다. 함정: 테스트를 시스템 바깥의 부수적 산출물로 취급하면, 프로덕션 코드에는 적용하던 설계 원칙을 테스트에는 면제해 준다 — 그 면제가 뒤에서 볼 취약한 테스트를 키운다.
취약한 테스트 문제
가장 독립적이어야 할 테스트가, 현실에서는 정반대로 시스템에서 가장 잘 부서지는 부분이 되곤 한다. 마틴은 이 병을 **취약한 테스트 문제(Fragile Tests Problem)**라 부른다. 증상은 이렇다. 시스템의 공통 컴포넌트 하나를 사소하게 바꿨을 뿐인데, 수백 개의 테스트가 우수수 깨진다. 바꾼 것은 한 줄인데 빨간불은 수백 개가 켜진다.
원인은 결합이다. 테스트가 시스템의 공통 구조에 강하게 결합되어 있으면, 그 공통 구조가 조금만 흔들려도 거기 매달린 테스트가 전부 함께 떨어진다. 가장 흔한 범인은 UI다. 화면을 통해 업무 규칙을 검증하는 테스트를 잔뜩 짜 두었다고 하자. 그러면 로그인 페이지의 버튼 위치를 옮기거나 필드 하나의 이름을 바꾸는 순간, 그 화면을 거쳐 가던 무관한 업무 규칙 테스트들이 죄다 깨진다. 검증하려던 규칙은 조금도 바뀌지 않았는데도.
이 지점에서 취약한 테스트는 자기 목적을 배신한다. 테스트는 변경을 안심하고 하기 위해 짜는 것이다. 그런데 취약한 테스트를 잔뜩 갖게 되면, 아주 작은 변경 하나가 수백 개의 테스트 수리라는 청구서를 달고 오므로, 개발자는 오히려 변경을 두려워하게 된다. 안전망이 되어야 할 테스트가 발목을 잡는 족쇄가 되는 것이다. 마틴이 냉정하게 짚는 결과는, 그 지경이 되면 팀은 테스트 수리에 지쳐 결국 테스트를 버리기 시작한다는 것이다.
판단 기준: 변경 한 건이 깨뜨리는 테스트의 수가 “그 변경이 바꾼 동작의 범위”에 비례하면 건강한 테스트다. 무관한 테스트까지 함께 깨진다면 결합이 잘못 놓인 것이다 — 2장에서 본 “변경 비용은 형태가 아니라 범위에 비례해야 한다”가 테스트에도 그대로 적용된다. 함정: 커버리지 숫자만 보고 UI를 통해 모든 것을 검증하는 테스트를 대량으로 짜면, 커버리지는 높아지지만 시스템에서 가장 자주 바뀌는 표면에 테스트 전체를 못 박아 두는 셈이 된다 — 숫자는 늘고 안정성은 떨어진다.
설계로 테스트를 짜야 한다
취약한 테스트는 테스트를 잘못 짠 결과가 아니라 테스트를 설계하지 않은 결과다. 마틴의 핵심 주장이 여기 있다. 테스트도 시스템의 컴포넌트인 이상, 프로덕션 코드에 적용하는 설계 원칙이 테스트에도 똑같이 적용되어야 한다. 테스트는 시스템에 맞춰 설계되어야 하고, 이때 목표는 다른 어떤 컴포넌트에서와 같다 — 결합을 끊어 변경의 파급을 좁게 가두는 것.
테스트를 설계 없이 짜면 어떻게 되나. 손 가는 대로, 눈에 보이는 표면(대개 UI)을 통해 검증하게 된다. 그것이 가장 쉬운 진입점이기 때문이다. 그러나 가장 쉬운 진입점은 대개 가장 변덕스러운 표면이기도 하다. 그래서 설계되지 않은 테스트는 자연히 가장 불안정한 것에 결합하고, 그 결과가 취약한 테스트다. 반대로, 테스트가 무엇에 결합할지를 의식적으로 선택하는 것 — 그것이 테스트를 설계한다는 말의 뜻이다.
그리고 이 설계의 방향을 정하는 규칙이 하나 있다. 마틴이 못박는 문장이다 — 변할 가능성이 높은 것에 테스트가 의존하지 않게 하라. 시스템에서 가장 자주 바뀌는 부분(GUI, 화면 구성, 세부 포맷)에 테스트를 매달지 말고, 상대적으로 안정적인 것(업무 규칙, 유스케이스)을 향해 결합을 놓아야 한다. 안정적인 것에 결합한 테스트는 안정적이고, 변덕스러운 것에 결합한 테스트는 변덕스럽다.
판단 기준: 어떤 테스트를 짜기 전에 “이 테스트는 무엇에 결합되는가, 그것은 얼마나 자주 바뀌는가”를 먼저 물어라. 업무 규칙을 검증하고 싶다면 UI가 아니라 업무 규칙 자체에 직접 결합하는 경로를 찾아라. 함정: “실제 사용자가 하는 대로 화면을 통해 검증해야 진짜 테스트”라는 믿음은 통합 테스트 한두 개에는 맞지만, 그 원칙으로 모든 규칙을 검증하려 들면 시스템 전체를 가장 취약한 표면에 결합시키는 과잉이 된다.
테스트 전용 API로 결합을 끊는다
그렇다면 테스트를 안정적인 것에 결합시키는 구체적 수단은 무엇인가. 마틴의 처방은 **테스트 전용 API(Test API)**다. 테스트가 시스템을 검증하기 위해 오직 테스트만을 위해 존재하는 별도의 API 계층을 두는 것이다. 테스트는 프로덕션 코드의 세부 구조에 직접 손을 뻗는 대신, 이 전용 API를 통해서만 업무 규칙과 대화한다.
이 API가 하는 일은 결합의 방향을 갈아 끼우는 것이다. 원래 테스트는 수백 개가 저마다 시스템의 내부 구조와 UI에 직접 결합되어 있었다. 테스트 API를 끼워 넣으면, 그 수백 개의 테스트는 이제 API 하나에만 결합하고, 시스템의 실제 구조에 대한 앎은 API 뒤로 숨는다. 구조가 바뀌면 API의 구현만 그에 맞춰 고치면 되고, 그 뒤에 매달린 수백 개의 테스트는 손댈 필요가 없다. 한 곳에서 흡수해 나머지를 지켜 주는 것 — 이것이 캡슐화가 늘 하던 일이며, 테스트 API는 시스템의 구조를 테스트로부터 캡슐화하는 장치다.
작은 그림으로 옮기면 이렇다. 취약한 테스트는 화면을 거쳐 규칙을 검증한다.
// 취약: UI 구조에 직접 결합했다. 버튼 이름이나 화면 흐름이 바뀌면 함께 깨진다.
loginPage.enterUsername("alice");
loginPage.enterPassword("secret");
loginPage.clickSubmit();
assertThat(dashboardPage.header()).contains("환영합니다");테스트 전용 API는 그 사이에 검증을 위한 언어를 하나 세운다.
// 안정: 업무 규칙에만 결합했다. UI가 어떻게 바뀌든 이 테스트는 그대로다.
testApi.givenRegisteredUser("alice", "secret");
LoginResult result = testApi.login("alice", "secret");
assertThat(result).isAuthenticated();UI를 뜯어고쳐도 아래쪽 테스트는 흔들리지 않는다. UI의 변덕은 testApi의 구현이 한 곳에서 삼키고, 수백 개의 규칙 테스트는 안정적인 업무 언어 위에 앉아 있기 때문이다.
판단 기준: 같은 검증을 여러 테스트가 반복하는 세부 절차(로그인 시퀀스, 데이터 준비)가 보이면, 그 절차를 테스트 API 뒤로 밀어 넣어 테스트가 “무엇을 검증하는가”만 말하고 “어떻게 접근하는가”는 모르게 하라. 함정: 테스트 API를 만든다면서 프로덕션 코드 안의 실제 구조를 그대로 노출하는 얇은 껍데기만 두르면, 결합은 하나도 끊기지 않고 이름만 바뀐다 — API는 세부를 숨겨야 의미가 있지, 세부를 그대로 통과시키면 없느니만 못하다.
구조적 결합과 보안적 결합을 함께 끊는다
테스트 API의 목적은 결합을 끊는 것 하나로 요약되지만, 마틴은 그 결합을 두 종류로 나눠 본다. 하나는 지금까지 이야기한 구조적 결합이다. 테스트가 프로덕션 코드의 클래스와 메서드 구조를 하나하나 알고 있으면, 그 구조를 리팩터링하는 순간 테스트가 깨진다. 테스트 API는 검증에 필요한 동작만 노출하고 구조는 감춤으로써, 구조를 자유롭게 바꿔도 테스트가 버티게 한다. 프로덕션 코드의 리팩터링과 테스트의 안정성이 분리되는 것이다.
다른 하나는 보안적 결합이다. 테스트 API는 시스템의 규칙을 검증하기 위해 평소라면 감춰 둘 위험한 통로(보안을 우회하는 지름길, 상태를 강제로 세팅하는 뒷문)를 열어 준다. 이 API가 프로덕션 시스템과 함께 배포되어 버리면, 그 뒷문이 그대로 공격 표면이 된다. 그래서 테스트 API는 반드시 프로덕션 코드와 분리된 컴포넌트에 두어, 운영 환경으로는 배포되지 않고 테스트 시점에만 존재하게 해야 한다.
판단 기준: 테스트 API를 별도 컴포넌트로 떼어 낼 수 있는지를 설계의 리트머스로 삼아라 — 떼어 낼 수 있다면 결합이 제대로 끊긴 것이고, 프로덕션 코드에 섞여야만 돈다면 아직 구조에 얽혀 있는 것이다. 함정: 편의를 위해 테스트용 뒷문을 프로덕션 클래스 안에 그냥 열어 두는 것 — 테스트는 통과하지만, 배포된 시스템에 인증을 건너뛰는 통로를 함께 실어 보내는 셈이다.
테스트는 설계를 위한 것이다
이 장을 관통하는 마지막 통찰은, 테스트 가능성이 결국 좋은 설계의 다른 이름이라는 것이다. 21장에서 이미 테스트 용이성이 아키텍처의 지표라고 짚었고, 23장의 험블 객체 패턴은 테스트하기 어려운 것과 쉬운 것을 갈라내는 일이 곧 경계를 긋는 일임을 보였다. 28장은 그 실을 이어받아 매듭짓는다 — 테스트를 시스템에 잘 결합시키려고 애쓰다 보면, 그 노력이 시스템 자체를 더 잘 분리된 구조로 몰고 간다.
왜 그런가. 테스트하기 어려운 코드는 대개 결합이 심하게 엉킨 코드다. 업무 규칙을 검증하려는데 반드시 데이터베이스와 UI와 프레임워크를 전부 깨워야 한다면, 그것은 업무 규칙이 그것들과 뒤엉켜 있다는 증거다. 테스트를 안정적으로 짜기 위해 그 규칙을 나머지에서 떼어내려 애쓰는 순간, 우리는 시스템에 필요했던 바로 그 경계를 긋게 된다. 그래서 마틴은 테스트를 설계 원칙에 따라 짜는 것을 강조한다. 그렇게 하지 않으면 테스트가 취약해질 뿐 아니라, 시스템을 더 낫게 설계하도록 이끄는 압력마저 잃기 때문이다.
테스트를 시스템의 일부로 대하는 것이 이 장의 처음이자 끝이다. 바깥의 검사원으로 두면 테스트는 설계에서 면제되고, 면제된 테스트는 가장 변덕스러운 표면에 매달려 취약해진다. 안쪽을 향해 의존하는 시스템의 컴포넌트로 대하면, 테스트는 다른 컴포넌트와 똑같이 결합을 끊는 설계의 대상이 되고, 그 과정에서 시스템 전체를 더 나은 구조로 끌어당긴다.
- 테스트는 시스템 바깥의 도구가 아니라 시스템의 일부이자 가장 바깥 원이다. 의존성 규칙을 따라 안쪽만 향하고, 아무도 테스트에 의존하지 않는다.
- 취약한 테스트 문제: 테스트가 공통 구조나 UI에 결합하면, 무관한 사소한 변경 하나가 수백 개의 테스트를 깨뜨려 안전망이 족쇄로 변한다.
- 해법은 테스트를 설계하는 것 — 변덕스러운 것 대신 안정적인 업무 규칙에 결합시키고, 그 사이에 테스트 전용 API를 세워 구조적·보안적 결합을 함께 끊는다.
- 테스트 API는 프로덕션과 분리된 컴포넌트에 두어 뒷문이 운영 환경으로 새어 나가지 않게 한다.
- 테스트 가능성을 좇는 일이 곧 경계를 긋는 일이다 — 테스트는 설계를 위한 것이다.
다음장으로 29장