본문 바로가기

개발자 강화/개발 독서

[Clean Code 2판] 1-14: Testing Disciplines

1-14장


Part1. Code

Chapter14. Testing Disciplines

 

과거의 테스트는 "동작 확인 후 버리는" 임시 코드에 불과했다.

저자도 90년대에 타이머 프로그램을 멜로디를 흥얼거리며 눈으로 확인하는 방식으로 테스트했을 정도로.

 

지금은 다르다. 테스트 더블로 외부 의존성을 격리하고, 테스트 코드도 프로덕션 코드만큼 정성껏 설계한다.

 

초판에선 TDD만 다뤘지만, 2판에선 세 가지 테스트 규율을 소개한다.

TDD 외에도 클린 코드와 양립 가능한 방식이 있다는 것을 인정한 것이다.


Discipline 1: Test-Driven Development (TDD)

켄트 벡이 1990년대 중후반에 제안했으며, 익스트림 프로그래밍 및 애자일 개발과 강하게 연결되어 있다.

TDD가 프로덕션 코드보다 단위 테스트를 먼저 작성한다는 건 잘 알려진 사실이다.

하지만, 단순히 테스트를 먼저 작성하는 게 전부는 아니다.

 

TDD는 단순한 원칙이 아니라 정교한 규율이다.

이를 잘 쓰는 사람들은 테스트를 다 만들어놓고 하나씩 통과시키는 방식이 아니라.

좀 더 advanced한 방법을 쓴다.


TDD의 세 가지 법칙

이 법칙들은 TDD의 근본 기반이며, 따르기가 매우 어렵다.

저자는 준비 없이 따르면 좌절하고 포기하게 될 것이라는 경고를 재차 남겼다.

(뭔데 대체 들어나 보자)

 

1. 실패하는 테스트를 먼저 작성하기 전에는 프로덕션 코드를 작성하지 않는다.

보통은 기능 구현 코드를 작성한 후 테스트 코드를 만들기 때문에, 이게 어색하게 느껴진다.

하지만, 발상을 역전시켜본다.

(역전재판이 생각난다. '발상을 역전시켜봐요 나루호도군..!')

 

프로덕션 코드를 작성할 수 있다면, 그 구조를 테스트하는 코드도 작성할 수 있다는 걸 의미한다.

단순히 순서가 어색하게 느껴질 뿐, 테스트를 먼저 작성하는 것도 충분히 가능하다.

 

2. 실패하거나 컴파일에 실패할 만큼만 테스트를 작성한다. 실패를 해소할 프로덕션 코드를 작성한다.

테스트의 첫 줄은 아직 존재하지 않는 코드와 상호작용하도록 작성된다.

그래서, 테스트를 한 줄도 쓰기 전에 프로덕션 코드로 옮겨가야 하는 경우가 많다.

 

3. 현재 실패하는 테스트를 통과시킬 만큼만 프로덕션 코드를 작성한다. 테스트가 통과되면 테스트 코드를 더 작성한다.

 

이 세 가지 법칙은 몇 초 단위의 아주 짧은 사이클로 묶인다.

 

1. 테스트 코드를 한 줄 작성한다. 당연히 컴파일 실패.
2. 테스트가 컴파일되도록 프로덕션 코드를 한 줄 더 작성한다.
3. 컴파일 되지 않는 테스트 코드를 한 줄 더 작성한다.
4. 테스트가 컴파일 되도록 프로덕션 코드를 한두 줄 더 작성한다.
5. 컴파일은 되지만 asserion에서 실패하는 테스트 코드를 한두 줄 작성한다.
6. assertion을 통과하는 프로덕션 코드를 한두 줄 작성한다.

 

Discipline 2: Test && Commit || Revert (TCR)

 

켄트 벡이 2018년에 제안한 규율이다.

테스트를 먼저 작성하도록 강제하지 않는 대신, 테스트를 통과한 코드는 즉시 커밋, 실패한 코드는 즉시 리버트하도록 강제한다.

소스 파일이 저장될 때마다 자동으로 실행되는 스크립트에 넣어서 이 결과에 따라 커밋/리버트가 자동으로 일어나게 한다.

 

실패 시 즉시 리버트 되기 때문에 개발자는 아주 작은 사이클로 step을 쪼개서 action할 수밖에 없다.

 

여기까지 읽고 진짜 코드 짤 때 개스트레스 받겠다 라고 생각했는데

저자도 이 TCR은 스트레스를 준다고 말했다. (ㅋㅋ)

저자는 TDD를 선호하는 편이지만, 테스트를 먼저 작성하는 쪽이 더 스트레스인 개발자는 TCR을 선호할 것이라고 말했다.


 

Discipline 3: Small Bundles

이 2판을 쓰는 과정에서 John Ousterhout가 제안한 방법이라고 한다.

 

TDD-TCR처럼 극도로 짧은 사이클을 강제하지 않는다.

사이클 단위가 초에서 분으로 늘어난다.

 

개발자가 특정 '결과'를 달성하기 위한 작은 코드와 테스트 묶음을 함께 작성한다.

코드를 먼저 작성할 수도 있고, 테스트를 먼저 작성할 수도 있고, 번갈아가며 할수도 있다.

 

다만 마무리할 때는 모든 테스트가 통과된 상태여야 한다.


 

Design

hammock-driven development

모든 소프트웨어 작업 전에 사전 사고는 필수적이다.

하지만, 코딩없이 몇 달간 전략 계획만 세우는 것도

전략 계획 없이 코딩만 하는 것도 모두 파멸의 길(??)이라고 한다.

 

적절한 전략적 설계 후에는 개발자가 전술적으로 전환해야 한다.

테스트 규율은 전술적 규율이며, 코드 작성이라는 전술을 보완한다.

 

테스트는 일종의 사용자이다.

테스트 규율을 바탕으로 사용자의 관점을 코드 설계에 강제해서 설계를 검증한다.

 

저자는 TDD가 최하위 레벨의 설계를 검증하는 데는 도움이 되지만,

더 높은 추상화 레벨의 설계나 아키텍쳐에는 도움이 되지 않는다고 한다.

 

 


 

Discipline

이런 테스트 규율을 고려해야 하지만, 이걸 맹목적으로 따라서는 안된다.

 

Tedious, Boring, and Slow

이 규율들이 while문이나 if문 하나를 작성하는데 흐름을 끊고 방해할 것 같다는 느낌이 들 수 있다.

 

Debugging

이 규율을 적극적으로 따른다면, 모든 코드는 불과 몇 분 전에 테스트를 통과했다.

모든 것이 항상 몇 분 전에 테스트를 통과했다면, 디버깅을 얼마나 하게 될까?

그저 마지막으로 동작했던 시점으로 리버트해서 다시 시도하기만 하면 될 것이다.

 

이 규율을 잘 따르면, 디버거를 거의 쓸 일이 없다고 말했다.

 

Documentation

테스트 규율을 따르면 작은 테스트들을 연속적으로 작성하게 된다.

각 테스트가 시스템의 어떤 부분이 어떻게 동작하는지 설명한다.

 

객체를 어떻게 생성하는지 알고 싶다면 - 객체 테스트 코드에 생성하는 방법이 모두 담겨있다

api를 어떻게 호출하는지 알고 싶다면 - api 테스트 코드에 호출 방법이 모두 담겨있다

 

이 테스트들이 시스템 전체에 대한 코드 예제이다

 

각 테스트는 서로 종속성이 없고 독립적이어서, 이해하기도 좋다.

 

테스트 코드는 문서에 포함된 예제와 다르게, 항상 프로덕션 코드와 일치한다.

주석이나 README는 코드와 싱크가 맞지 않을 수 있지만, 테스트는 통과하는 한 항상 현재 동작을 정확히 반영한다.

 

고수준의 문서는 아니지만, 시스템의 최하위 레벨에서는 훌륭한 문서가 된다.

 

Reliability

신뢰성

 

테스트 규율을 따르지 않는 프로그래머의 시나리오

1. 모듈 작성 완료, 수동 테스트 통과
2. '단위 테스트도 작성해야 해요' -> 이미 테스트했으니 귀찮
3. 대충 몇 개 작성해보니, 통과됨
4. 테스트하기 어려운 모듈 등장 (테스트를 고려하지 않고 설계했으니)
5. 재설계? 시간 없음. 수동 테스트 되는 것 확인함
6. 테스트 작성을 포기하고 떠남

 

이렇게 test suite에 구멍이 생긴다.

이런 구멍 투성이 테스트가 통과한다면? 모두가 이게 '시스템 동작을 보장'한다는 의미가 아닌 걸 안다.

하지만, 모두가 외면한다. 그래서, 그냥 넘어간다.

 

반면, 테스트 규율을 따르면?

이 테스트 통과가 높은 확신을 함께 가져다 준다.

그러면 중요한 결정(배포)을 안전하게 할 수 있다.

 

Design

테스트 규율을 따르면,

테스트하기 어려운 모듈을 작성할 수 없게 된다.

코드와 함께 테스트를 작성하니, 자연스럽게 모든 모듈을 테스트하기 쉽게 설계하게 된다.

 

테스트 하기 쉬운 모듈은 결합도가 낮기 때문에, 시스템의 전체적인 설계도 개선된다.

각 모듈이 서로에게 받는 영향이 최소화된다.


Summary

테스트 규율을 따르면 이와 같은 효과를 얻는다.

1. 디버깅 시간이 크게 줄어든다.
2. 거의 완벽한 저수준 문서가 지속적으로 만들어진다.
3. 통과 시 배포도 가능한 수준의 신뢰할 수 있는 test suite가 만들어진다.
4. 결합도가 낮은 시스템이 설계된다.

 

이건 테스트 규율의 positive effect이지만, purpose는 아니다.


코드를 청소하는 것에 대한 두려움

1장 클린 코드에서 엉망진창 코드는 생산성을 갉아먹는다고 했고,

보이스카우트 룰이 유일한 해결책이라고 했다.

그런데, 코드를 어떻게 깨끗하게 유지할까?

 

엉망인 코드를 작업하려고 컴퓨터 앞에 앉았다.

난데없이 천사와 악마가 등장한다 (진짜 뜬금없음)

 

😇: 청소하면 돼!

😈: 안돼! 건드리지마! 건드리면 망가지고, 망가지면 네 책임이야!

 

코드를 청소하는 것에 두려움이 생겨 방치한다.

코드는 결국 그대로 남아 팀 생산성을 끌어내린다.

 

저자의 일침:

코드가 이렇게 통제 불능 상태가 되도록 내버려 뒀다는 것이 얼마나

"비전문적이고 무책임한지"

진지하게 생각해보라.

창조자인 당신이 창조물에 대한 통제력을 잃고, 그 변덕에 종속됐다.

ㄷ..ㄷ

 

Muzzling the Devil

같은 시나리오에서,

만약 좋은 테스트 규율로 훌륭한 test suite를 구축했다면?

 

엉망인 코드를 발견했다.

😇: 청소해!

그리고, 악마는 힘을 잃어 나타나지 않는다.

 

1. 변수 이름 하나 변경 -> 테스트 실행 -> 통과 -> 아무것도 망가지지 않음

2. 긴 함수를 둘로 분리 -> 테스트 실행 -> 통과

3. 새 함수를 다른 클래스로 이동 -> 테스트 -> 즉시 리버트 -> 통과

4. 다시 올바른 위치로 이동 -> 통과

 

test suite가 있고, 신뢰할 수 있고, 빠르게 실행된다면 "코드를 청소할 수 있게" 된다.

청소에 대한 "위험이 없기" 때문이다.

(리팩토링 하나 했다 장애 터지고 리버트 한 악몽이 떠오름.,.,,)

 

팀 전체가 보이스카우트 룰을 따르며 지속적으로 코드 품질을 개선한다.

 

테스트 규율의 진짜 이유는 코드를 청소할 용기를 주는 것이다.

(개인적인 생각이지만, 그럴 시간을 주는 것도 중요하다고 생각한다.)


Complications and Loopholes

복잡한 경우와 허점들

 

방금의 이야기는 이상적인 시나리오다.

현실은 그렇게 다순하지 않다. 테스트가 불가능하거나, 비실용적인 상황(ROI가 안나옴)이 있다.

 

1. 하드웨어 경계

화면 출력, 마우스 움직임, 소켓 통신 같은 하드웨어 경계 외부와 통신하는 코드는 테스트하기 거의 불가능하다.

 

Humble Object Pattern

테스트가 불가능한 코드 구간을 최대한 얇게 만들고,

로직을 하드웨어 경계에서 멀리 이동시킨다.

경계에 접촉하는 코드는 최대한 단순하게 만든다.

 

(FE 개발도 마찬가지다

DOM 조작과 비즈니스 로직을 분리하는 것,

React에서 커스텀 훅과 UI 컴포넌트를 분리하는 것)

 

2. 서드파티 프레임워크

 

프레임워크가 testable하게 작성되지 않았다면, 그걸 쓰는 코드도 테스트하기 어렵다.

 

저자는 Java Swing으로 규모가 꽤 있는 앱을 만들었는데, Swing에 닿는 코드를 테스트하기 어려웠다고 한다.

테스트된 컴포넌트/테스트 안 된 Swing 컴포넌트로 분리했지만

Swing API가 콜백을 담은 크고 서로 연결된 구조를 강제해서 테스트 안되는 부분이 엄청 커졌다고 한다.

 

테스트로 70%의 커버리지가 최선이었으며, "I won’t use Swing again." (ㅋㅋㅋㅋㅋ)

 

3. 주관적인 결과

 

폰트가 제대로 보이는지, 필드가 올바른 위치에 있는지, 보고서가 좋아 보이는지

이건 다 "주관적"인 기준이다.

 

코드가 맞는지는 직접 보고 판단하는 수밖에 없다.

이런 건 테스트를 작성할 수 없다.

 

저자의 질문:

AI의 결과물은 어떻게 테스트하는가?

 

테스트하기 비실용적인 영역(ROI가 안나오는 부분)이 있다면, 그 영역을 최대한 작고 단순하게 유지하고,

나머지 시스템이 그 영역으로부터 보호받을 수 있게 설계해라.

 

Costs and Repercussions

비용과 파장

이 테스트 규율을 충실히 따르면, 매일 수십 개 - 매월 수백 개 - 매년 수천 개의 테스트를 만들게 된다.

프로덕션 코드와 맞먹는 양이 만만치 않은 관리 포인트가 된다.


Keeping Tests Clean

저자가 코칭했던 한 팀의 이야기다.

'테스트 코드를 프로덕션 코드와 동일한 품질 기준으로 관리하지 않기로 명시적으로 결정'한 팀이었다.

 

Quick and dirty

그들의 모토였다.

(이 모토 뭔가 익숙한데)

 

변수 이름? 대충.

함수가 짧고 설명적? 상관없음.

설계? 필요없음.

 

So long as the test code worked,

and so long as it covered the production code,

it was good enough.

 

테스트 코드가 동작하고, 프로덕션 코드를 커버하기만 하면 된다고 여겼다.

그 팀은 지저분한 테스트여도 없는 것보단 낫지-라고 생각했겠지

 

하지만, 그 팀이 깨닫지 못한 것은

지저분한 테스트는 없느니만 못하다.는 사실이었다.

 

테스트가 복잡하게 얽혀있을수록 새로운 테스트를 suite에 쑤셔넣는데 시간이 걸린다.

새 프로덕션 코드를 만드는 것보다 그게 더 시간이 오래걸릴 수 있다.

 

프로덕션 코드를 수정하면 기존 테스트들이 실패하기 시작한다.

엉망인 테스트 코드가 그 테스트들을 다시 통과시키기 어렵게 만든다.

테스트는 이제 관리되지 않고 부채가 된다.

 

배포를 거듭할수록 test suite 유지 비용이 증가했다.

개발자들은 테스트를 병목으로 여겼고, 결국 완전히 폐기해야 했다.

 

하지만, test suite을 아예 폐기하자,

코드 베이스 변경이 예상대로 작동하는지 확인하기 어려워졌다.

 

의도하지 않은 장애가 증가했고,

코드 청소가 득보다 실이 커진다는 느낌이 들어 코드 청소를 멈췄다.

 

테스트 없음, 버그로 가득찬 프로덕션 코드, 실망한 고객.

 

결국 테스트를 엉망으로 내버려둔 결정이 이 모든 것의 발단이었다.

 

테스트 코드는 프로덕션 코드만큼 중요하다.

사고, 설계, 주의가 필요하다.

프로덕션 코드처럼 깨끗하게 유지해야 한다.


 

Tests Enable the -ilities

테스트가 코드를 유연하고(flexible), 유지보수 가능하며(maintainable), 재사용 가능하게(reusable) 유지해준다.

 

테스트가 있으면 코드를 개선하는게 두렵지 않기 때문이다.

 

아키텍처가 아무리 유연하고, 설계가 아무리 잘 분리되어 있어도,

테스트 없이는 모든 변경이 잠재적 버그가 된다.

 

테스트가 변경을 가능하게 한다.

그리고 변경이 모든 -ility를 가능하게 한다.

 

(영어에서 ility로 끝나는 단어들, 소프트웨어 품질 속성을 묶어서 표현하는 관용적 표현이다)

  • flexibility (유연성)
  • maintainability (유지보수성)
  • reusability (재사용성)
  • testability (테스트 가능성)
  • reliability (신뢰성)

 

테스트 지저분 -> 코드 변경 능력 손상 -> 코드 구조 개선 불가 -> 테스트 상실 -> 코드 썩음

 

테스트를 깨끗하게 유지해야 하는 이유다.


작성; 2026.02.25. 00:50

728x90