[Study - TDD] TDD(Test-Driven Development, 테스트 주도 개발) 이란
TDD가 무엇인지와 좋은 테스트를 하려면 어떻게 해야되는지 간단하게 알아보자.
TDD 란
TDD(Test-Driven Development, 테스트 주도 개발)는
반복 테스트를 이용한 소프트웨어 방법론으로,
작은 단위의 테스트 케이스를 먼저 작성하고, 나중에 이를 통과하는 코드를 추가하는 단계를 반복해서 구현한다.
왜냐하면 이 방식은 개발 초기 단계에서 버그를 발견하고 수정하는 데 도움을 주기 때문이다.
즉, 짧은 개발 주기의 반복에 의존하는 개발 프로세스이며,
애자일 방법론 중 하나인 XP의 Test-First 개념에 기반은 둔 단순한 설계를 중요시한다.
XP (eXtreme Programming)
미래에 대한 예측을 최대한 하지 않고, 지속적으로 프로토타입을 완성하는 애자일 방법론 중 하나로,
추가 요구사항이 생겨도 실시간으로 반영할 수 있다.
따라서 TDD는 개발자가 기능을 구현하기 전에 요구 사항을 명확하게 이해하도록 강제하며,
이는 결과적으로 더 깨끗하고 유지보수하기 쉬운 코드로 이어진다.
또한, TDD는 소프트웨어의 설계를 개선하고, 리팩토링을 촉진하는 효과적인 방법으로 작용하기 때문에
단순한 테스트 작성 방법론을 넘어서, 소프트웨어 개발의 전반적인 품질을 향상시키는 중요한 전략이다.
TDD 기본 원칙, 절차
TDD를 실천하기 위한 기본 원칙에는 3가지 주요 단계가 있다.
1
2
3
4
5
6
@Test
public void test() {
// 1. 실패하는 테스트 작성
// 2. 테스트를 통과하는 최소한의 코드 작성
// 3. 코드 리팩토링
}
실패하는 테스트 작성
- 개발자는 새로운 기능에 대해 테스트 케이스를 먼저 작성한다.
- 이 테스트는 처음에는 실패해야 한다.
- 그 이유는 해당 기능에 대한 구현이 아직 이루어지지 않았기 때문이다.
테스트를 통과하는 최소한의 코드 작성
- 실패한 테스트를 통과하기 위해 필요한 최소한의 코드를 작성한다.
- 이 단계에서는 오직 테스트를 통과하는 것만을 목표로 한다.
코드 리팩토링
- 코드를 리팩토링하여 중복을 제거하고, 가독성을 높이며, 설계를 개선한다.
- 이 과정에서 추가적인 테스트를 작성할 수도 있다.
TDD 장점, 단점
TDD 장점
- 코드의 품질과 유지보수성이 향상된다.
- 개발 초기 단계에서 버그를 발견하고 수정할 수 있어서, 장기적으로 시간과 비용을 절약할 수 있다.
- 개발자에게 더 나은 설계와 구현 방법을 고민하게 만든다.
이것이 추후에는 소프트웨어의 전반적인 아키텍처에 긍정적인 영향을 미친다.
TDD 단점
하지만 TDD를 실천하는 데는 몇 가지 단점이 있다.
테스트를 먼저 작성하는 것이 익숙하지 않은 개발자에게는 초기 러닝커브가 있을 수 있다.
또한, 테스트를 작성하고 유지하는 데 추가적인 시간과 노력이 필요하다.
그래도 장기적인 측면에서 TDD는 위와 같은 단점들을 상쇄한다고 생각한다.
테스트 목적
테스트를 작성함으로써 얻는 이득과 노력을 항상 고려해서 테스트에 드는 노력을 최소화 해야한다.
예를 들어 오래된 기존 코드를 전부 뜯어가며 테스트를 추가하는 것은 투자 대비 생산성이 좋지 않다.
좋은 테스트란
- 무작정 시간을 들여 작성하는 것이 아니라, 개발 주기에 통합이 있어야 한다.
- 코드베이스에서 핵심 부분만을 테스트 한다.
- 테스트 대상이 아닌 것 : 인프라, 외부 시스템(DB, 서드파티, 라이브러리), 모든 것을 하나로 묶는 통합코드
- 최소한의 유지비로 최대의 가치를 끌어내야 한다.
- 아무런 검증도 하지않고, 어떤 가치도 만들어 내지않는 코드나, 그저 커버리지만 채우기 위한 테스트는 필요없다.
단위 테스트
단위 테스트는 작은 코드 단위를 빠르게 테스트 할 수 있고, 격리된 방식으로 자동화하는 테스트이다.
즉, 테스트하는 대상에 집중해야 하고, 다른 외부의 문제가 영향이 끼치지 않도록 격리해야 한다.
따라서 객체지향을 사용하게 되면, 각각의 객체들로 서로 다른 의존성을 통해 문제를 해결하게 한다.
이상적인 테스트
이상적인 테스트는 좋은 테스트인지 전혀 가치없는 테스트인지 확실하게 구분할 수 있는 아래 3가지가 골고루 갖춰져야 한다.
- 회귀방지
- 리팩터링 내성
- 빠른 피드백과 유지보수성
회귀방지
회귀는 일종의 소프트웨어 버그이다.
정확히는 새로운 기능을 추가했을 때, 의도한 대로 작동하지 않아 이미 완성된 코드를 수정하려고 돌아가는 것이다.
프로젝트가 커지면서 소프트웨어의 복잡도는 빠르게 증가한다.
이런 회귀를 방지하고자 미리 테스트를 해야 한다.
테스트를 통과했는데 기능은 고장나는 False Negative를 만들지 말자.
회귀하지 않도록 가능한 모든 경우를 테스트로 검증하자.
즉, 회귀 오류가 생길일이 없는 코드를 테스트하지 말자.
예를 들어 Member.getName()
처럼 아무 로직이 없는 코드를 테스트하는 것은 테스트 할 가치가 없다.
리팩터링 내성
테스트는 코드 리팩터링에 내성이 있어야 한다.
새로 기능을 추가했고, 잘 동작하는데 기존 테스트가 깨지면 안된다.
이런 경우는 테스트의 가치를 떨어뜨리고, 결국 개발자는 테스트 수정을 피하기 위해서 코드 변경을 최소화하려고 하게 된다.
테스트가 리팩토링에 내성이 있으려면 구현 세부사항이 아닌 최종 결과를 테스트 목표로 삼으면 된다.
회귀방지에서는 가능한 모든 경우를 테스트해야 되는데 리팩터링 내성에서는 뭐가 다를까?
예를 들어 PasswordEncoder
같은 내부 알고리즘이 바뀌었다고 테스트 결과에 영향이 가지 않는다.
즉, 테스트 할 때 그 객체의 내부 동작까지 알 필요없이 공개된 동작을 실행하고, 그 결과만 확인하면 된다.
만약 구현의 세부사항이 변경되었을 때 테스트가 깨질 수 있다면, 괜찮아 보이지만 False Positive 테스트가 아닌가 의심해야 한다.
대부분의 경우 회귀방지에서의 False Negative는 예민하지만, 리팩터링 내성에서의 False Positive에는 둔감하다.
그 이유는 개발 초기에는 False Positive가 큰 문제가 되지 않기 때문이다.
하지만 프로젝트가 커질수록 리팩터링 내성은 중요해진다.
기능 고장이 아닌데도 테스트가 계속 깨지면서 테스트에 대한 신뢰를 읽게되고, 개발자는 의무감에 작성하는 가치없는 테스트를 계속 작성하게 된다.
빠른 피드백과 유지보수성
단위 테스트에서 빠른 피드백은 매우 중요하다.
모든 의존성을 다 검사하는 것이 아닌, 그 대상만을 빠르게 검증하고 버그를 피드백함으로써 비용을 크게 줄일 수 있다.
이미 충분한 검증을 했다면 새로운 기능을 추가하더라도 기존 코드가 깨지지 않을까 두려워하지 않아도 된다.
단위 테스트는 유지보수성이 좋아야 한다.
테스트를 이해하기 쉽고 외부 의존성없이 바로 실행하기 쉽게 만듦으로써 나중에 테스트를 추가하더라도 테스트의 유지보수가 쉬워진다.
결론
TDD는 소프트웨어 개발의 품질과 효율성을 향상시키는 강력한 방법론이다.
이유는 코드의 품질을 높이고, 버그를 줄이며, 유지보수를 용이하게 하기 때문이다.
만약에 개발 초기에 신속함을 원해서 TDD는 불필요하다고 생각할 수 있고, 실제로 TDD를 작성함으로써 초기 개발 속도는 떨어질 수 밖에 없다고 생각한다.
하지만 TDD를 작성하지 않은 상태로 빠르게 개발한 후에 오류가 발생한다면? 혹은 운영중에 장애가 발생한다면?
이런 이슈를 해결하기 위해 항상 처음부터 테스트를 해나가야 해서 시간이 오래걸릴 것이고, 유지보수 측면에서도 코드나 구조를 쉽게 파악할 수는 없을 것 같다.
개발 초기부터 시간은 좀 더 걸리지만 TDD를 구현해 나간다면? 예상치 못한 오류나 운영중의 장애 발생 시 이미 작성해 놓은 테스트가 있기에 빠른 대처가 가능할 것 같고,
유지보수 측면에서도 코드나 구조에 대해 빠른 파악이 가능할 것 같다.
테스트 코드를 작성할 때 의존성이 없도록 작성하고, 로직이 없는 코드처럼 필요없는 코드는 작성하지 말자.
참고
Inpa - TDD 방법론 (테스트 주도 개발) - 알기 쉽게 정리
F-Lab - 테스트 주도 개발(TDD)의 이해와 실천