brunch

You can make anything
by writing

C.S.Lewis

by 백명석 Mar 24. 2022

TDD에 대한 몇 가지 질문

아기 발걸음

작년 패스트캠퍼스 레드에 "개발자의 성장, 코드 리뷰, 레거시, TDD" 등에 대해서 최범균 님과 함께 강의를 했다. 

https://fastcampus.co.kr/dev_red_bcr

위 강의는 슬랙으로 질문을 받고 있는데 지난번에는 코드 리뷰에 대해서 블로그로 작성할 만한 질문이 있었고, 이번에는 TDD에 대한 좋은 질문이 있어서 이 글을 작성한다.


Q1. 테스트 코드를 리팩터링 해야만 하나? 

테스트 코드는 대개의 경우 프로덕션 코드보다 읽기 어려워지는 경향이 있는 것 같다. 

그래서 후에 테스트 코드의 이해를 쉽게 하기 위해 테스트 코드의 리팩터링이 필수라고 생각한다.

내 경우 테스트 코드를 작성할 때는 일단 컴파일되고 돌아만 가게 빨리 구현하려고 한다. 그러다 보면 테스트 메서드 하나가 길어져서 시간이 지난 후에 읽을 때 이해에 어려움이 생기는 경우가 많다. 따라서 동작은 하지만 주욱 나열되어 후에 빠르게 이해하기 어려운 테스트 코드를 작은 함수들로 추출하는 리팩터링을 통해 쉽게 이해할 수 있도록 개선하는 리팩터링이 필수적이다.

테스트 케이스가 길어져서 이해에 어려움이 생기는 것 외에 

테스트 케이스들 간의 중복이 많이 생겨서 리팩터링이 필요하다. 

특히 Arrange(given) 부분에서 

테스트할 대상 객체 생성

테스트할 메서드에 전달할 인자 생성

등에서 중복이 많이 생긴다. 이와 관련해서 생성 메서드(Creation Method), 팩토리 클래스, 오브젝트 머더(Object Mother) 등을 적용하면 도움이 된다. 아래 글에 조영호 님의 작성한 멋진 글을 정리한 자료가 있다.

https://github.com/msbaek/memo/blob/master/DSL-and-UnitTest.md

중복 제거에 대해서 조심해야 할 것(진짜 / 가짜 중복)이 있다. 

Robert C. Martin의 Clean Architecture에서 보면 중복은 진짜 중복(True Duplication)과 가짜 중복(False Duplication)이 있다. 

진짜 중복은 중복된 코드 블록 한 곳을 수정할 때마다 반드시 중복된 다른 부분도 수정해야 하는 경우이다. 즉 결합된(Coupled) 경우이다. 이런 중복은 반드시 제거해서 향후 변경 시 한 곳만 고쳐서 문제가 되지 않도록 해야 한다. 물론 중복을 제거하면 코드가 더 간결해지기도 한다.

가짜 중복은 우연한 중복(Accidental Duplication)이라고도 한다. 즉 처음에는 중복으로 보였지만 시간이 지나면서 서로 다르게 변경되어 나중에는 중복이 아닌 경우이다. 이런 경우는 중복 제거를 성급하게 하면 오히려 복잡도가 늘어난다.

그럼 어떻게 진짜 중복과 가짜 중복을 구분할 수 있을까? 

늘 조금씩만 변경해서 언제든지 중복 제거가 가능하고, 또 이전에 제거된 중복을 다시 없앨 수 있어야 한다. 중복을 미리 예상해서 복잡한 작업(클래스 추출, 추상화 적용 등)을 수행하지 말아야 한다. Martin Fowler는 이와 관련해서 "Rule of three"라는 규칙을 언급했다. 중복 제거에 대한 원칙으로서 중복이 2군데 존재할 경우는 리팩터링이 필요 없지만, 3군데 발생하면 리팩터링을 통해 중복을 제거해야 한다는 규칙이다. 



Q2. 테스트 코드를 리팩터링 하면 테스트를 위한 로직이 늘어나서 유지보수 부담이 증가할까?

이 질문은 사실 한 번도 생각을 안 해봤다. 테스트 케이스를 리팩터링 하면서 생성된 코드의 유지보수 부담이라? 작고, 이해하기 쉽게 코드를 관리하고, 테스트 케이스를 통해서 늘 수행되어 커버된다면 유지보수 부담은 없을 것 같다.


Q3. given / when / then을 작성하는 순서가 어떤 영향을 미치나?

given, when, then이라고도 하고, AAA(arrange, act, assert)라고도 하는데 AAA로 내 의견을 기술해 보겠다. 코드를 위에서부터 아래로 작성하는 것이 의식의 흐름과 방향이 같아서 편안하게 느껴진다. 하지만 이런 경우 대개 필요한 것 이상을 코딩하게 된다. 반대로 아래(assert)에서 위로 작성하면 IDE의 도움을 받아서 꼭 필요한 코드만 작성하게 된다. IDE를 활용하여 더 안전하고, 빨리 코딩할 수 있기도 하다.

Kent Beck은 TDD를 이름을 잘못 지었다고 했다. 

Test Driven Design이나 Test First Development라고 했어야 했다고...(Rober C. Martin은 프로덕션 코드를 먼저 작성하고 테스트를 추가하는 방법을 TAD(Test After Development)라고 한다).

테스트를 먼저 작성하는 TDD의 경우는 assert부터 작성하는 것이 생산적이라고 생각한다. 툴의 도움을 받으며 반드시 필요한 코드만 작성하게 된다.

하지만 이미 작성된 프로덕션 코드(레거시거나 TAD 방식인 경우)에 테스트를 추가하는 경우라면 arrane부터 작성하게 되는 것 같다. 이 경우는 어떤 클래스와 어떤 메서드가 필요한지 이미 알고 있고, 또 프로덕션 코드가 이미 존재하기 때문에...

이 순서로 작성해야만 불필요한 코드 작성을 최소화하고 꼭 필요한 코드만 작성하게 됩니다.

Q5. 어떤 순서로 테스트 케이스를 추가하나?

"As the tests get more specific, the code gets more GENERIC"

위 문장을 강조하면서 Robert C. Martin은 가장 단순한 케이스부터 추가하라고 한다. 단순한 케이스는 단순한 코드로 성공시킬 수 있다. 테스트 케이스가 점점 특정(specific) 케이스로 진화하면 프로덕션 코드는 점점 다양한 경우를 성공시킬 수 있게 포괄적(generic)으로 진화한다. 

이와 관련해서 삼각법(Triangulation)이 종종 인용된다. 

TDD에서 삼각법은 generalization을 만드는 방법의 하나를 의미한다. 

삼각법은 하나의 큰 테스트가 아니라 여러 개의 작은 테스트를 추가함으로써 문제와 해결책을 좀 더 명확히 하는 기법이다. 삼각법이 2개 이상의 지점의 위치를 이용하여 현 위치를 측정하는 것처럼...

하나의 테스트만 존재할 때는 fake 할 수 있다(상수를 반환함으로써). 하지만 상수로 처리 불가한 테스트를 추가하면(삼각법에서 2개 이상의 지점을 사용하는 것처럼) fake 할 수 없게 된다.

이 방법의 동작 원리는 아래와 같다.

단순한 케이스는 대개 빠르게 성공시킬 수 있다.

처음부터 아주 특화된 케이스(우리가 원하는 구현물을 검증할 수 있는 테스트)를 추가하면 대개의 경우 프로덕션 코드는 매우 견고해져 이후 다른 케이스가 추가될 때 변경하기 어려워진다.

또, 

"아기 발걸음(Baby Steps)" from Kent Beck. "익스트림 프로그래밍"

을 알아볼 필요가 있다. "아기 발걸음"은 올바른 방향으로 움직일 수 있는 일 중 최소한의 일은 무엇일지 생각하면서, 많은 작은 단계빠르게 밟아 나가라는 원칙이다. 단계를 쪼갤 때 생기는 부하(overhead)가 큰 변화를 시도했다가 실패해서 다시 원상태로 돌아갈 때 드는 낭비보다 훨씬 작다고 보기 때문에 이 원칙이 동작한다. 본래 수행해야 할 중요한 기능을 한 번에 구현하려 들지 말고, 아주 사소한 기능(테스트)부터 구현하기 시작하여 점점 좀 더 복잡한 기능을 추가하다면 본래 구현하려던 중요한 기능은 후에 쉽게 구현되는 경우가 많다.

http://blog.cleancoder.com/uncle-bob/2013/05/27/TheTransformationPriorityPremise.html

아기 발걸음과 관련해서 위 글이 도움이 될 것이다.

하지만 우리는 구현을 시작할 때 우리가 원하는 중요한(복잡한) 기능이 제일 먼저 생각난다. Kent Beck의 자료를 보면 그는 종종 처음 생각난 중요하고 복잡한 기능을 위한 테스트 케이스를 작성하고, 바로 코멘트 처리하고, 그보다 쉬운 단순한 기능부터 진행한다. 

https://youtu.be/c-Pv2ia05Ek?t=138


위 영상이 Kent Beck의 방식을 보여준다. 이 영상은 Kent  Beck이 만든 원본 동영상을 보고 Kent Beck의 동의를 구해 만든 영상이다(관련 github repo).

Q5.  업무가 주어지면 전반적인 구현 방안이 머리에 그려지는데...  그래서 TAD를 하는데... TFD를 해야 하나?

이미 머리에 전반적인 구현 방법이 있다고 하더라도 향후 변경에 유연하게 대응할 수 있는 좋은 설계의 코드를 만들려면 TFD가 필수적이라고 생각한다. 이미 프로덕션 코드에 어떻게 작성하면 될지 알더라고 그 코드를 작성해야만 하도록 테스트 코드를 먼저 추가해야 한다. 뭔가를 하고 싶다면 먼저 할 필요가 있게 만들어야 한다. 나는 이 방식을 "필요 기반 개발(NDD - Needs Driven Development)"라고 말한다 ^^

이런 "아기 발걸음", 테스트를 먼저 추가하는 방식은 매우 짧은 호흡(실패하는 테스트 추가 → 성공하도록 프로덕션 코드 추가 → 리팩터링)의 TDD 사이클을 통해 좋은 설계로 이어진다. 또한 늘 피드백(동작, 성공 여부)을 받을 수 있어서 안정감을 가지고 개발에 몰입할 수 있다.

따라서 TAD를 하고 있다면 어렵더라도 꼭 TFD로 방식을 변경했으면 한다. TAD를 하면 지금 방금 작성한 코드인데요 테스트 추가가 용이치 않은 경우가 발생한다. 원인은 테스트를 먼저 만들지 않았기 때문이다. 테스트를 먼저 만들면 테스트를 추가하기 어려운 경우는 발생 자체가 불가하다. 그리고 테스트하기 쉬운 코드는 테스트가 첫 번째 코드의 사용자가 되므로 사용하기 쉬운 코드가 된다. 이러한 코드는 대개 좋은 설계를 갖게 된다.

아래 글에 정리한 내용들이 NDD, TFD를 하는데 도움이 될 것이다.

https://github.com/msbaek/memo/blob/master/AdvancedTDD.md

결론

지금 구글에서 "kent beck baby step"으로 검색하니 

https://twitter.com/kentbeck/status/1260938674440515585?lang=en

위 글이 나온다. "아기 발걸음으로 해라. 진짜 진짜 빠른 아기 발걸음으로. 아기 발걸음이 먼저다"라는 Kent Beck의 트윗이다. "아기 발걸음"은 작게 나눠서 실행하지만 너무너무 빠른 발걸음이어서 어른 발걸음(?) 보다 느리지 않은 것이라고 여겨진다. 하지만 작게 나눠서 실행했기 때문에 수정 비용이 매우 낮은 효율적이면서 효과적인 방법이라고 생각한다.

새로운 뭔가(예. 코드 리뷰, TDD)를 도입할 때 처음부터 잘하기 어렵다. "아기 발걸음"이다. 작게 나눠서 빠르게 실행하는 것을 점진적으로 반복한다면 멀지 않아 고수가 되어 있을 것이라고 생각된다.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari