[도서 기록] 테스트 주도 개발 - 켄트백 정독하기(1)
해당 글은 켄트 백 저자의 테스트 주도 개발을 읽고 그 내용을 주관적으로 정리한 글입니다.
"테스트 주도개발은 현재 하나의 기술 스택으로 추앙받고 있으나 그 본질은 자신의 사고에 달려있다.
자신의 코드를 자신이 검증하며 스스로를 돌아볼 수 있는 기회로 삼아야 한다는 것 이다."
테스트 주도 개발이란?
테스트 주도 개발이란 반복된 테스트로 자신이 만든 기능을 검증하고 이를 최대한 작은 단위로 만들어 이를 통과하는 코드들을 추가하며 개발하는 것을 말합니다.
이러한 테스트 주도 개발을 잘하기 위해선 켄트백은 3가지 단계를 거쳐야한다고 얘기합니다.
그 첫 번째 단계는 규칙을 충실히 지켜보는 것입니다.
간단하고 쉬운 문제들로 TDD를 시도하면서 프로그래밍 하기 쉬운 문제들로 TDD의 구조에 익숙해지는 것이 첫 단계입니다.
두 번째 단계는 자신만의 방법을 찾아 자신의 코드에 대해 분석하고 새로운 영역에도 TDD를 적용해보는 것입니다.
세 번째 단계로는 TDD를 사용하지 않고 같은 문제에 접근하여 TDD의 필요성을 체험해보는 것입니다. 그 후 초기단계부터 TDD를 접목시켜보는 것입니다.
해당 단계들을 연습하다보면 TDD가 지향하는 궁극적인 목표인 "작동하는 깔끔한 코드"를 만들 수 있을 것이라고 말합니다.
작동하는 깔끔한 코드가 궁극적인 목표로 꼽히는 이유는 여러가지가 있지만 제일 중요한 핵심 두 가지는 다음과 같습니다.
- 예측이 가능한 개발방법이다.
- 코드가 주는 교훈을 학습할 기회가 생기게 된다.
여기서 예측이 가능한 코드라는 말은 테스트 코드로 자신의 코드를 검증하며 작은 단위로 개발 하기 때문에 버그에 대비할 능력이 늘어나 미래에 유연한 대처가 가능하여진다는 것을 의미합니다.
코드가 주는 교훈이라는 것은 자신의 코드에 대해 스스로가 검증을 진행해야 하기에 스스로에게 계속하여 피드백을 던져 더 효율적인 코드를 추구할 수 있다는 말을 뜻합니다.
또한 TDD에는 두가지의 절대적 규칙이 존재합니다.
1. 오직 자동화 된 테스트코드가 실패할 경우에만 새로운 코드를 작성한다.
2. 중복을 제거한다.
해당 말은 번역본에서 따와서 다소 어색한 부분이 존재하나 주관적으로 해석해보자면 테스트 코드는 실행가능하면서 유기적이어야하고 테스트 환경에서 사용되는 컴포넌트들은 응집도는 높고 결합도는 낮게 구현해야합니다.
그리고 모든 테스트 코드는 중복되지 않고 한 테스트는 하나의 기능만을 검증해야합니다.
이 두가지 규칙에 의해 생겨난 프로그래밍 개발 순서도 존재합니다. 바로 신호등 개발 방법론으로서 빨강,초록,리팩터링 순으로 진행을 해야합니다.
빨강 - 실패하는 작은 테스트를 작성한다. 처음 테스트를 작성하며 기능의 요구사항에 맞추며 테스트를 작성하면서 기능을 개발한다.
초록 - 테스트가 빠르게 통과하게 만들어 이를 위해선 함수가 특정값만을 반환하게 하여도 괜찮다.
리팩터링 - 일단 테스트를 통과하게 만든 과정중 생겨난 모든 중복을 제거한다.
사실 말이 너무 길어져 복잡하지만 서두에서 말하는 테스트 주도 개발의 정의는 다음과 같습니다.
자신 스스로에게 제약을 주어 기능에 대한 고민을 통해 스스로에게 피드백을 던져 실패가 없는 서비스를 창출하는 것을 말한다.(책에서는 일주일동안 A4용지에 적어가며 구상한 후 테스트 코드를 작성하는 것 또한 TDD라고합니다)
1부 화폐 예제
1부에서는 완전히 테스트에 의해 주도되는 전형적 모델 코드를 개발하는 것이 목표입니다.
하지만 우리는 코딩을 시작하기전 구현해야할 사항들을 문서로 기술하는 절차를 갖도록 하겠습니다.
구현해야할 목록에는 볼드체를 넣어 표시해줍니다.
구현이 완료돤 목록에는 취소선을 넣어 표시해줍시다.
자 그럼 ,이번 1부에서 구현해야할 목표를 살펴봅시다.
$5 + 10CHF = 10$
$5 * 2 = 10$
인스턴스 변수 private으로 생성
Dollor 클래스의 부작용
Money = 환율 반올림?
그 중 한 부분인 간단한 환율에 따른 환전 코드를 살펴보도록 해보자면 다음과 같습니다.
@Test
@DisplayName("Dollor 객체의 환율 계산 테스트 ")
void Dollor_exchange_rate(){
Dollor five= new Dollor(5);
int exchange = five.times(2);
assertThat(exchange).isEqualTo(10);
}
현재 이 코드를 보면 무슨 생각이드시나요? Dollor 라는 객체의 자기 생성자로 dollor의 단위 값을 받고 times라는 메서드로 곱 연산을 하는게 떠오르시나요?
그렇다면 틀렸습니다.
현재 이는 앞서 말했듯 틀리기 위한 코드입니다.
Dollor 객체는 존재하지 않을 뿐더러 금액과 금액의 곱계산을 해야하는데 int 형의 변수를 사용하고 있습니다.
우리는 현재 여기서 4가지 오류를 만날 수 있습니다.
- Dollor 객체가 존재하지 않음
- 자기 생성자 존재하지 않음
- times 메서드 존재하지 않음
- 인스턴스 변수 존재하지 않음
이렇듯 먼저 테스트 코드를 작성한 후 오류를 차근차근 고쳐나가며 개발을 하는 것이 테스트 주도개발의 시작입니다.
이 과정에서 우리는 의존성과 중복을 조심해야합니다.
의존성은 모든 프로그래밍 과정에서 생겨날 수 밖에 없는 과정으로 객체가 변하면 테스트 코드를 수정해야한다는 점이 그 대표적인 예시입니다.
그래서 최종적으로 우리가 지향해야하는 것이 의존성이 낮은, 한번의 수정으로 또 다른 수정을 낳지않는 코드를 지향해야합니다. 중복은 그 이후의 문제로 보고 있습니다.
중복을 제거한 다는 것은 다음과 같습니다.
assertThat(exchange).isEqualTo(10);
이 한줄의 메서드를 만족시키기 위해서 편법부터 부려봅시다.
int exchange = 5 * 2;
로 해당 메서드를 만족시킬 수 있다.
여기서 중복을 제거해보면 다음과 같이 변환 시킬 수 있습니다.
private int exchange;
public int times(int target){
return unit*target;
}
이렇게 작은 단계를 거치며 우리는 머릿속에서 상상만 하던 최종코드를 완성시키게 됩니다.
package dollor;
public class Dollor {
private int unit;
public Dollor(int unit) {
this.unit = unit;
}
public int times(int target){
return unit*target;
}
}
정말 간단한 코드이지만 테스트 주도개발 방법론을 적용시키면 이렇게 복잡한 과정을 거친다는 것입니다.
정말 단순한 작업이지만 우리는 생각보다 많은 과정을 거쳤습니다.
1. 요구사항분석
2. 이를 목록화시키고 컨벤션 생성
3. 테스트코드를 직관적으로 표시
4. 빠르게 통과하는 테스트 코드를 통과
5. 일반화 시켜보기
이렇게 점진적인 방법론이 커다란 웹 프로젝트 또는 API 서버에 적용된다면 그 의미는 달라질 것이라고 생각합니다.
다음 일반적인 TDD 주기를 켄트백이 얘기합니다. 상당히 장황하게 설명되어있지만 요약하자면 다음과 같이 정리됩니다.
1. 테스트를 작성한다. 단 이때 자신이 원하는 결과 값을 포함시키어 작성한다.
2. 해당 테스트를 실행 가능하게 만든다. 실행 가능하게 만든다는 것이 꼭 반드시 완벽한 코드일 필요는 없고 아까와 같은 5*2 같은 방법이어도 상관없다. 단, 해당 코드를 오래 동안 유지하여선 안된다.
3. 올바른 코드로 리팩터링한다. 편법들을 수정하고 옳바른 코드로 수정시킨다.
이 부분에서 알고리즘 기법에서 자주 보이는 분할정복과 유사한 부분을 보입니다.
앞서 말했던 작동하는 깔끔한코드에서 "작동하는"에 먼저 초점을 맞춘뒤 "깔끔한 코드"로 만드는 것이 분할정복의 작은 기능부터 구현하여 최종적으로 큰기능을 만드는 것과 유사합니다.
해당 예제에서 우리는 객체를 값처럼 사용하고 있다. 이러한 패턴을 값 객체 패턴이라고 한다.
이처럼 값 객체 패턴의 특징이자 장점은 변수명에 대해 고민하지 않아도 되며 그 객체는 불변한다는 점입니다.
해당 예시로 Dollor에 5라는 값을 넣어준 five라는 객체는 무한에 가까운 시도를 하여도 5달러라는 값으로 고정된 객체이기에 불변성을 띄고 있습니다. 만약 7달러로 테스트를 하고 싶다면 새로운 객체를 생성하여 seven이라는 Dollor타입의 객체를 생성해야합니다.
이러한 점은 test코드를 작성할 때도 유의해야할 점으로 값 객체 패턴을 사용할 경우에는 객체의 특성을 잘 이해한 상태를 기반으로 사용하여야합니다.