

TDD는 테스트 코드를 작성하고, 이를 기반으로 개발의 효율성을 높이는 개발 방법이다.
우테코에서 TDD를 제대로 배우기 전, TDD에 대해 회의적인 생각을 가지고 있었다.
그랬던 것도 당연한게 test에 어색했었고, 아키텍쳐를 구상하는 것이 익숙하지 않은 상태에서 TDD를 막연하게 알고 도입하려니까 당연한 결과였다.
최근에 읽은 <객체 지향의 사실과 오해>에서는 TDD를 다음과 같이 설명하고 있다.
- 중요한 것은
테스트를 작성하는 것이 아니라책임을 수행할 객체 또는 클라이언트가 기대하는객체의 역할이 메세지를 수신할 때 어떤 결과를 반환하고 그 과정에서 어떤 객체와 협력할 것인지에 대한 기대를 코드의 형태로 작성하는 것이다.- 즉,
객체 구현에 집중하기 보다, 필요한 기능(투입, 결과), 흐름을 코드로 작성해보면서 큰 그림을 짜는 것이다.- 즉, TDD는 책임-주도 설계의 기본 개념과 다양한 원칙과 프로세스, 패턴을 종합적으로 이해하고
좋은 설계에 대한 감각과 경험을 길러야만적용할 수 있는 설계 기법이다. 역할, 책임, 협력에 집중하고 객체지향의 원칙을 적용하려는 깊이 있는 고민과 노력을 통해서만 TDD의 이점을 누릴 수 있다.
이 책에서 내가 지금까지 TDD를 꺼려했던 이유를 단번에 이해할 수 있었다!
하지만 지금은 TDD에 대해 나름 괜찮은 느낌을 가지고 있고, 효율적인 개발론 중에 하나라고 생각한다.
왜냐하면 TDD의 본질을 깨달았기 때문이다.
💡 문제를 작은 단위로 정의하고, 피드백을 자주 받으면서 해답을 찾는 과정
1. 빨강 - 실패하는 작은 테스트를 작성한다. 처음에는 컴파일조차 되지 않을 수 있다.
2. 초록 - 빨리 테스트가 통과하게끔 만든다. 이를 위해 어떤 죄악을 저질러도 좋다.
3. 파랑 - 일단 테스트를 통과하게만 하는 와중에 생겨난 모든 중복을 제거한다.
TDD가 어려운 이유는 바로 1단계 부터 나같은 초보에게는 어렵게 느껴지기 때문이다.
작은 테스트를 작성하라니… 아니 우선 어떤 객체안에 넣을 건지 구상부터 해야 하는거 아니야..?
무슨 테스트 부터 작성하란 말이야?
사실 맞다. 위의 <객체 지향의 사실과 오해> 말을 다시 인용해 보자면
💡 즉, TDD는
객체 구현에 집중하기 보다, 필요한 기능(투입, 결과), 흐름을 코드로 작성해보면서 큰 그림을 짜는 것이다.
TDD는 객체의 흐름을 이미 알고 있다는 가정하에 큰 틀을 만들어 가는 과정이다. 즉, 이미 아키텍쳐를 대충이라도 설계를 해놓거나, 테스트를 짜면서 아키텍쳐를 짤 수 있는 능력이 있어야 가능하다는 뜻이다.
나의 경우는 당연히 전자이기에, TDD 에 들어가기 전에 구현을 미리 생각해놓고 1단계에 들어가야 겠다는 결론에 도달했다. 물론 이는 반은 맞고 반은 틀린 말이다. Test를 하면서도 설계가 가능하기 때문이다.
예를 들어, 이번 우테코 미션이었던 <로또 게임> 을 생각해보자.
우선 요구사항을 도메인 별로 정리해본다.
로또
- 로또의 숫자의 범위는 1-45
- 6개 숫자로 구성
- 중복되지 않는다.
- 로또 번호는 오름차순으로 정렬해 보여준다.
...
이와 같이 잘 정리된 요구사항은 TDD의 첫 대상을 시작하는 청사진이 된다.
test('로또의 숫자 범위는 1-45이다', () => {
expect(new Lotto([1,2,3,4,5,50])).toThrow('[ERROR]')//에러 나는 테스트 만들기
})
위와 같이 요구사항에 맞춰 작은 단위의 테스트 를 만든다.
여기서 중요한 것은 한번에 모든 요구사항을 다 만족시키려고 하지 않고, 작은 단위의 테스트부터 차근차근 만들어 보는 것이다.
위의 Lotto 클래스는 당연히 구현이 안된 상태이다. 이제부터 Lotto 클래스를 대충 구상해보는 것이다.
class Lotto{
constructor(numbers){
if (numbers.some(number => number < 1 || number > 45)) throw new Error('[ERROR]')
}
}
바로 위의 코드처럼 로또 클래스를 만들고, 테스트가 통과하도록 코드를 대충 짠다!
이 코드는 테스트를 통과하기 위한 코드이므로 나중에 무조건 리팩토링의 대상 이 된다. 그러니 두려워하지말고 코드를 만들어보자
TDD의 장점으로 언급되는 빠른 피드백 이란 무엇일까? 🤔
💡 즉, TDD는
객체 구현에 집중하기 보다, 필요한 기능(투입, 결과), 흐름을 코드로 작성해보면서 큰 그림을 짜는 것이다.
TDD의 목적은 사실 ‘작은 단위 테스트를 구현하는 것’ 이지만, 목적은 큰 그림을 그리는 것이라고 했다.
우승 로또를 만드는 법을 생각해보자.
우승로또 (당첨번호, 보너스 번호)
- 로또의 숫자의 범위는 1-45
- 6개 숫자로 구성
- 중복되지 않는다.
- 로또 번호는 오름차순으로 정렬해 보여준다.
- 보너스 번호로 구성
test('우승 로또의 숫자 범위는 1-45이다', () => {
expect(new winLotto([1,2,3,4,5,50],10)).toThrow('[ERROR]')
//에러 나는 테스트 만들기
})
아마 일반 로또랑 비슷하니까 다음과 같이 작성했을 것이다.
class winLotto{
constructor(numbers, bonusNumber){
if (numbers.some(number => number < 1 || number > 45)) throw new Error('[ERROR]')
if (bonusNumber < 1 || bonusNumber > 45)) throw new Error('[ERROR]')
}
}
하지만 이는 Lotto 클래스와 중복되지 않는가?
bonusNumber를 검사하는 로직만 제외하면… 그렇다면 Lotto 클래스를 활용해 WinLotto의 인자로 넣어주면 어떨까?
class winLotto{
constructor(<Lotto>, bonusNumber){
if (bonusNumber < 1 || bonusNumber > 45)) throw new Error('[ERROR]')
}
}
위같이 첫 인자에 Lotto인스턴스를 받는 방식으로 바꿔 코드의 중복을 줄이고, Lotto와 WinLotto의 관계가 더 잘 보이는 구성이 되었다.
이런식으로 test는 대충 그린 밑 그림을 더 진하게 만드는 ‘스케치’의 역할을 한다.
작은 단위인 테스트를 만드는 것은 그림에서 작은 사물을 얕게 그리는 것이고, 이것을 잘 배치하는 작업 (구상 작업)을 통해 프로덕션 코드의 스케치를 완성하는 것이다.
아까 이 말이 반은 맞고 반은 틀리다고 했던 이유가 여기서 나온다.
나의 경우는 당연히 전자이기에, TDD 에 들어가기 전에 구현을 미리 생각해놓고 1단계에 들어가야 겠다는 결론에 도달했다.
구현이 어색한 사람은 TDD를 하기 전에 큰 그림을 짜는게 맞다. 어짜피 요구사항을 정리하는 것은 개발에서 0순위이기 때문에, 그것을 하면서 큰 그림을 대충 그려보는 것이다. 하지만 제대로 구성하는 것은 Test를 하면서 해도 충분하다!
test를 짜면서 어떤 객체에 어떤 역할을 부여할지 큰 그림을 더욱 구체화 하여 그려보는 것이다!
TDD를 제대로 공부하기 전에는 테스트의 역할이 단순히 개발자의 노동력을 아껴주고, 성공 커버리지를 위한 코드라고 생각했다.
하지만 TDD를 제대로 공부하고 활용해보니, 테스트는 프로덕션 코드 후에 짜야 하는 귀찮은 것이 아닌, 프로덕션 코드를 도와주는 도우미 역할을 할 수 있다는 것을 깨닫게 되었다. 어짜피 테스트를 짜야 한다면, 구현 단계부터 짜면서 도움을 받자!
아직 내가 100% TDD를 활용하진 못하지만, 많은 사람들이 극찬하는 이유를 알 것 같다. 더욱 실력을 길러 제대로 활용해 봐야지!