본문 바로가기
디자인패턴, 설계

TDD(Test Driven Development)

by 흥부와놀자 2021. 9. 7.

TDD(Test Driven Development)란?

기존에 우리는 개발을 할때 코딩을 먼저 한 뒤 테스트를 수행한다. 하지만 TDD는 이 순서를 바꿔서 개발한다.

이말인 즉슨 테스트를 먼저 진행한 뒤 코드를 짠다는 것인데...

대체 뭘로 어떻게 테스트 한다는 것일까?

 

예를 들어 집을 짓기위해 벽돌을 쌓는다고 하자. 벽돌을 쌓기 전에 일정 분량의 틀을 만들어 놓고 만들어 놓은 틀까지 벽돌을 쌓는다. 틀이 다 채워지면 벽돌이 틀에 잘 채워졌는지 체크하고 수정작업을 거친 뒤 다시 새로운 틀을 만들고 벽돌을 쌓는 행위를 반복한다고 한다고 할때, 이러한 방식이 TDD이다. 

 

미리 틀을 만드는것은 테스트 코드를 작성하는 것이고 벽돌을 쌓는것은 테스트 코드에 맞는 실제 코드 작성을, 틀에 잘 채워졌는지 체크하고 수정작업을 거치는것은 코드 작성 후 리팩토링 하는 과정이 된다.

 

https://wikidocs.net/224

1. 뭘 만들지 정하기

2. 테스트 코드 작성

3. 구현 코드 작성

4. 작성된 코드 리팩토링

 

 

그렇다면 예제를 통해 실제로 어떤 프로세스로 TDD가 이뤄지는지 알아보록 하자.  

예제로 구현할 기능은  알파벳 대문자를 소문자로 바꿔주는 기능이다.

 

https://ebbnflow.tistory.com/271

1. 실패하는 코드를 작성하여 실패하는 테스트 만들기

가장 첫단계로, 앞으로 만들 테스트코드가 의미있을지 확인 하는 과정이다. (쉽게 설명하기 위해 의사코드로 작성하였다)

//실패를 위한 간단한 구현코드
GetSmall(인풋){
	return A
}

//테스트코드 -> isEqual은 첫번째 매개변수와 두번째 매개변수가 같은지 테스트하는 테스트코드이다.
isEqual(a,GetSmall(A))
		.
		.
		.
isEqual(z,GetSmall(Z))

우리가 구현해야할 코드인 GetSmall은 지금 A를 반환하기 때문에, 즉 아직 구현코드가 미작성 됬기 때문에 밑의 테스트코드는 무조건 틀렸다고 나올것이다.

이렇게 하는 이유는 새로운 기능을 작성하기 전에 혹시 이전에 작성했던 기능이나 어떤 이유로 인해 이 테스트의 결과가 맞다고 나오거나 컴파일에러가 날수도 있기 때문이다. 이렇게 실패하는 테스트를 검증함으로써 앞으로 작성할 테스트 코드가 무의미해 질수 있는 버그들을 확인할 수 있다. 

 

2. 통과하는 코드를 작성하기

그럼 1번의 과정으로 작성한 테스트코드의 최소한의 신뢰성을 얻었으니 이젠 통과하는 코드를 만들어 봐야 한다. 

여기서 중요한건 가장 빠르고 간단하게 만들 수 있는 코드를 작성해야 한단것이다. 

//성공를 위한 간단한 구현코드
GetSmall(인풋){
	if(인풋===A) 
    	return a
    	else if(인풋===B) 
        return b
        .
        .
        .
    	else if(인풋===Z) 
        return z
}

//테스트코드 -> isEqual은 첫번째 매개변수와 두번째 매개변수가 같은지 테스트하는 테스트코드이다.
isEqual(a,GetSmall(A))
		.
		.
		.
isEqual(z,GetSmall(Z))

들어오는 대문자에 대해 소문자를 일일히 분기처리하여 리턴시켰다. 물론 우리는 당연히 저것보다 조금 더 괜찮은 로직을 이용해 구현이 가능하다. 하지만 이렇게 예시를 적은 이유는 TDD를 하는데 있어서 아주 중요한 점을 말한다.

예시의 기능은 아주 간단한 기능이지만 만약 어려운 기능을 구현할때, 단계별 테스트 없이 곧바로 더 복잡한 로직을 적는 다는것은 그만큼 버그가 날 수 있는 위험을 포함하는 것이다. 지금 까지 나역시 이런식의 위험을 알고도 그저 성공을 지레짐작하며 개발을 해왔다. 하지만 TDD에선 좀더 세세한 테스트를 거쳐 차곡차곡 완성의 코드로 나아가는 것을 지향한다. 그렇기에 최대한 해당 테스트를 간신히 통과할 정도로만 구현하라고 권고한다.

 

3. 코드 리팩토링하기

2번의 과정을 거친 코드를 리팩토링 할 수 있어야 한다. 코드에는 실제 구현코드와 테스트 코드 둘다 포함된다.

//성공를 위한 간단한 구현코드
GetSmall(인풋){
		return (char)((number)인풋+32)
}

//테스트코드 -> isEqual은 첫번째 매개변수와 두번째 매개변수가 같은지 테스트하는 테스트코드이다.
isEqual(a,GetSmall(A))
		.
		.
		.
isEqual(z,GetSmall(Z))

 

개발자는 위의 코드가 비효율적이라 느끼고 인풋의 아스키코드를 계산하여 소문자로 만드는 방식으로 리팩토링 하였다. 이런 리팩토링에선 기존 구현코드안에 있는 불필요한 중복 등을 제거하는 과정이라고 할 수 있다.

 

 

4. 새로운 기능 테스트 코드 추가

개발자는 인풋값에 대문자가 아닌 값이 들어올 경우를 생각해냈다고 하자.

//성공를 위한 간단한 구현코드
GetSmall(인풋){
		return (char)((number)인풋+32)
}

//테스트코드 -> isEqual은 첫번째 매개변수와 두번째 매개변수가 같은지 테스트하는 테스트코드이다.
isEqual(a,GetSmall(A))
		.
		.
		.
isEqual(z,GetSmall(Z))
isEqual(-1,GetSmall(i))
isEqual(-1,GetSmall(j))

이러한 버그를 막기 위해 바로 구현코드를 작성하는게 아닌 테스트 코드를 먼저 작성한다. 대문자가 아닌 값의 경우 에러표시로 -1이 나오도록 하였다. 이 상태로 테스트코드 실행 시 테스트가 실패하게 되고, 처음 1처럼 해당 테스트 코드의 신뢰성을 검증받게 된다.

 

5. 새로운 테스트코드에 맞는 구현코드 생성

//성공를 위한 간단한 구현코드
GetSmall(인풋){
		if(65<=(number)인풋<=90)
		return (char)((number)인풋+32)
		else
        	return -1
}

//테스트코드 -> isEqual은 첫번째 매개변수와 두번째 매개변수가 같은지 테스트하는 테스트코드이다.
isEqual(a,GetSmall(A))
		.
		.
		.
isEqual(z,GetSmall(Z))
isEqual(-1,GetSmall(i))
isEqual(-1,GetSmall(j))

이러한 여러 과정을 거쳐서 결과적으로 최종적 코드가 완성된것을 볼 수 있다.

정리를 하면, TDD의 정석 프로세스는 1. 어떤 기능을 구현할지 정하고 2. 그에 대한 실패테스트를 만들어서 테스트코드를 검증하고 3. 해당 테스트코드를 간신히 통과할 정도의 구현코드를 만들고 4. 리팩토링 시킨다. 또한 지속적으로 테스트 코드들을 만들어야 하기 때문에 테스트 코드 작성에 많은 시간이 걸려서도 안된다.

 

물론 이런 과정들이 처음엔 귀찮게 느껴지고 불필요하게 생각될 수 있다. 특히 완료 일정에 대한 압박이 있다면 더 그럴 것이다.

그럼 왜 이렇게 귀찮은 일을 하는걸까?

시간이 지나면서 소프트웨어의 크기는 커지고 점점 복잡해졌다. 기존의 폭포수 개발방식의 한계가 드러나기 시작했는데, 개발에 들어간 후 요구사항이 변경되거나, 요구사항 자체가 개발에 바로 적용할 수 있을만큼 구체적이지 않음으로 인해 일정예측이 어려워 지는 등 한계가 나타났다. 이런점을 극복하고자 초기 설계비용을 줄이고 개발 프로세스의 주기를 짧게하여 극도의 효율을 이끌어내는 XP(Extream Programming)이 나왔다. 이 XP에선 매번 프로토타입을 만들어내야했고, 어떻게 하면 버그없이 완벽한 프로토타입을 그때그때 만들어낼 수 있을까? 에서 나온게 이 TDD인 것이다. 즉 테스트를 구현보다 앞서 진행함으로써 개발자가 통제못하는 코드의 위험성을 줄임으로써 문제없는 XP를 할 수 있는 원동력으로 작용 하는것이다.    

 

이렇게 TDD를 사용하며 얻을수 있는 점들은

1. 로직이 깔끔해진다.

실제로 나올 수 있는 테스트 케이스들에 대한 구현 로직들로 이루어 졌고 계속 리팩토링을 거치기 때문에 꼭 필요하고 깔끔한 코드를 생산할 수 있다.

 

2. 최소한 개발자가 생각해낼 수 있는 예상가능 버그들은 없앨 수 있다.

아무리 TDD라 하더라도 개발자가 생각해내지 못한 버그의 위험은 어쩔수 없지만 최소한 예상가능한 버그들은 없앨 수 있다.

 

3. 코드를 완성했다고 할 수 있는 기준이 생긴다.

기존에는 내가 구현한 코드가 완성된 코드인지 몰라서 개발 일정이 항상 불확실했지만 

생각해낼 수 있는 테스트코드를 작성하고 그에 대한 구현코드 작성을 마칠때, 해당 기능에 대한 구현을 완료 했다고 할 수 있는 기준이 생김으로써 개발 스케쥴이 명확해진다. 

 

4. 불안함을 없앨 수 있다.

개인적으로 가장 크다고 느낀것으로 기존에는 언제 어떻게 버그가 날지, 또는 이 버그를 어떻게 해결할지 항상 불안해 하며 개발을 했었지만 TDD를 사용한다면 기존의 불안한 마음이 사라지고 심신이 안정된 상태로 꾸준히 개발에 임할 수 있을거라고 생각된다. 

 

이러한 TDD를 하기 위해 극복해야 할 점들이 존재한다. 

1. 기존과 다른 방식이기에 방식을 바꾸는게 쉽지가 않다. 

 

2. TDD 프로세스의 틀에 얽매이면 안된다.

사실 위의 예시는 TDD를 보여주기 위해 극단적인 예시에 불과하다. 꼭 TDD라고 하여도 저런 프로세스를 하나하나 다 지켜가면서 할 필요는 없다고 생각한다. 위의 예시처럼 코드가 너무 간단한 경우엔 굳이 테스트 코드를 작성하지 않는게 더 좋은 선택일 것이다.