티스토리 뷰
이번에는 여러 개발 서적들 및 실무 경험 그리고 시행 착오 등을 겪으면서 얻은 테스트 주도 개발 방법에 대해 소개해보고자 합니다.
이번 포스팅에서는 먼저 단위 테스트와 중요성 그리고 단위 테스트를 바탕으로 하는 TDD는 어떻게 하는 것인지 이론적으로 소개해보고자 합니다. 이어지는 포스팅들에서 실제로 테스트에 주도되는 개발을 해볼 것인데, 많은 분들에게 도움이 되기를 바라겠습니다:)
1. 단위 테스트의 중요성과 좋은 단위 테스트의 특징
[ 단위 테스트(Unit Test)를 작성해야 하는 이유 ]
단위 테스트를 작성해야 하는 이유는 정말 너무 많다. 그 중에서 몇 가지 핵심적인 이유들을 작성하면 다음과 같다.
- 코드를 수정하거나 기능을 추가할 때 수시로 빠르게 검증 할 수 있다.
- 리팩토링 시에 안정성을 확보할 수 있다.
- 개발 및 테스팅에 대한 시간과 비용을 절감할 수 있다.
테스트 코드를 작성하면 우리가 작성한 코드들에 대해 수시로 빠르게 검증을 받을 수 있으며, 유지보수 및 리팩토링을 할 때에도 안정성을 확보할 수 있다는 장점이 있다. 하지만 그것보다 큰 장점으로 개발 및 테스팅에 대한 시간과 비용을 절감할 수 있다는 점에 주목해야 한다.
우리는 개발이 끝난 뒤에 문제가 없는지 확인하기 위해 애플리케이션을 실행하고, 직접 수동 (통합) 테스트를 진행해야 한다. 단위 테스트를 작성하지 않은 코드들은 테스트를 작성하지 않은 코드들 보다 버그가 있을 확률이 높은데, 문제는 직접 테스트 하는 비용이 너무 크다는 것이다. 그 이유는 통합 테스트를 위해서는 캐시, 데이터베이스 등 외부 컴포넌트들과 연결 등 부가적인 시간이 필요하기 때문이다.
테스트 코드를 작성하지 않았다면 여러 개의 버그가 잠재되어 있을 확률이 있고, 모든 버그들을 수정하고 테스트를 반복하는 비용은 기하급수적으로 늘어나게 된다. 그러므로 우리는 개발 및 테스팅에 대한 비용을 줄이기 위해 단위 테스트를 작성해야 한다.
[ 좋은 테스트의 특징 ]
그렇다고 테스트를 무작정 작성하는게 좋은 것은 아니다. 좋은 테스트를 작성해야 그 이점을 누릴 수 있는데, 좋은 테스트의 특징은 FIRST라는 5가지 규칙을 따라야 한다. 아래의 내용들은 CleanCode에 자세히 나와있다.
- Fast: 테스트는 빠르게 동작하여 자주 돌릴 수 있어야 한다.
- Independent: 각각의 테스트는 독립적이며 서로 의존해서는 안된다.
- Repeatable: 어느 환경에서도 반복 가능해야 한다.
- Self-Validating: 테스트는 성공 또는 실패로 bool 값으로 결과를 내어 자체적으로 검증되어야 한다.
- Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.
즉, 위의 내용을 압축하여 정리하면 좋은 테스트는 빠르게 독립적으로 어느 환경에서든 실행이 가능하고 검증할 수 있어야 한다는 것이다. 만약 테스트를 실행하는 비용이 크다면 그럴 바에는 통합테스트를 하는게 더 나을 수도 있다.
그리고 우리는 위의 5가지 규칙 중에서 Timely에 주목해야 할 필요가 있다. 테스트 코드를 작성하는 시점에 대한 이야기인데, CleanCode에서는 테스트 코드를 실제 코드를 구현하기 직전에 구현하라고 설명하고 있다. 즉, 테스트 코드를 먼저 작성하라는 것이다. 우리는 왜 테스트 코드를 먼저 작성해야 하고, 어떻게 테스트 코드를 먼저 작성할 수 있는지에 대해 알아야 한다.
(단위 테스트와 관련해서는 다른 포스팅에서 다루어보았습니다. 이와 관련해서 부족하신 분들은 이 글을 참고해주세요!)
2. TDD(테스트 주도 개발), 테스트 코드를 먼저 작성해야 하는 이유와 방법 및 순서
[ 테스트 코드를 먼저 작성해야 하는 이유 ]
테스트 코드를 먼저 작성하는 개발 방법론은 테스트 주도 개발(Test-Driven Development, TDD)로 많이 불린다. 우리는 프로덕션 코드 보다 테스트 코드를 먼저 작성해야 하는데, 그 이유는 다음과 같다.
- 깔끔한 코드를 작성할 수 있다.
- 장기적으로 개발 비용을 절감할 수 있다.
- 개발이 끝나면 테스트 코드를 작성하는 것은 매우 귀찮다. 실패 케이스면 더욱 그렇다.
TDD의 궁극적인 목표는 작동하는 깔끔한 코드를 작성하는 것이다.TDD의 개발 단계에는 리팩토링이 있는데, 이 리팩토링 과정을 거치면서 중복된 코드들은 제거되고, 복잡한 코드들은 깔끔하게 정리하게 된다. 또한 테스트를 처음 작성할 때에는 귀찮고 개발을 느리게 한다는 느낌을 받을 수 있지만, 장기적으로 보면 반드시 개발 비용을 아껴줄 것이다.
마지막으로는 정말 현실적인 이야기인데, 프로덕션 코드를 먼저 작성하였다면 이후에 테스트 코드를 작성하는 과정은 너무 귀찮다. 왜냐하면 테스트 코드는 성공 케이스 뿐만 아니라 실패 케이스까지 작성해야 하기 때문에 작성해야 할 테스트의 개수는 해당 함수에서 발생가능한 모든 경우들인 N이며, 이미 개발이 완료되었기에 끝났다는 심리적 요인 때문에 테스트를 작성하는 것이 꺼려지기 때문이다. (물론 그렇지 않은 사람도 있을 수 있지만, 저의 경우는 매우 그렇습니다.) 그렇기 때문에 특별한 경우가 아니라면 테스트 코드를 먼저 작성하는 것이 좋다.
그리고 그 중에서도 실패 테스트부터 작성해야 한다. 즉, 순차적으로 실패하는 테스트를 먼저 작성하고, 오직 테스트가 실패할 경우에만 새로운 코드를 작성해야 한다. 그리고 중복된 코드가 있으면 제거를 하는 것이다.
[ TDD(Test-Driven Development, 테스트 주도 개발) 방법 및 순서 ]
TDD 개발 방법론의 프로그래밍 순서는 매우 단순하다.
- 실패하는 작은 단위 테스트를 작성한다. 처음에는 컴파일조차 되지 않을 수 있다.
- 빨리 테스트를 통과하기 위해 프로덕션 코드를 작성한다. 이를 위해 정답이 아닌 가짜 구현 등을 작성할 수도 있다.
- 그 다음의 테스트 코드를 작성한다. 실패 테스트가 없을 경우에만 성공 테스트를 작성한다.
- 새로운 테스트를 통과하기 위해 프로덕션 코드를 추가 또는 수정한다.
- 1~4단계를 반복하여 실패/성공의 모든 테스트 케이스를 작성한다.
- 개발된 코드들에 대해 모든 중복을 제거하며 리팩토링한다.
위의 과정을 따라 개발을 진행하면 자연스럽게 프로덕션 코드보다 테스트 코드를 먼저 작성하게 될 것이다. 물론 이론적으로만 해당 내용을 이해하기는 어렵고, 나의 경우도 그러하였다. 또한 이를 Spring과 같은 프레임워크에 적용하는 것은 다른 영역이고, 이러한 이유로 ATDD(Application TDD)라고 불린다. 그러므로 Spring에서는 어떻게 TDD를 적용할 수 있는지 알아보도록 하자.
[ TDD(Test-Driven Development, 테스트 주도 개발) 접근 방법 ]
- 가짜로 구현하기: 최대한 빨리 테스트를 통과하기 위해 정답이 아닌 가짜 정답을 구현하는 방법
- 삼각측량법: 값이 다른 여러 테스트를 작성하고, 이를 일반화하여 정답을 구현하는 방법
- 명백하게 구현하기: 정답을 바로 구현하는 방법
가짜로 구현하기
실패하는 테스트를 가장 빠르게 구현하는 방법은 아무 값이나 반환하도록 하는 것이다. 그리고 테스트가 통과하면 단계적으로 상수를 변수를 사용하도록 변형한다. 예를 들어 다음과 같은 곱하기 테스트를 작성하였다고 하자.
@Test
public void 곱하기테스트() {
// given
// when
final int result = multiply(2, 3);
// then
assertThat(result).isEqualTo(6);
}
해당 테스트를 가장 빠르게 통과하는 방법은 6을 반환하는 것이다.
public int multiply(final int num1, final int num2) {
return 6;
}
이렇게 변수를 사용하지 않고 상수를 반환하며, 답이 아닌 방법으로 가짜 구현하여 최대한 빨리 테스트를 통과하는 것이 가짜 구현 방법이다. 가짜 구현으로 연습을 많이 해두면, 복잡한 코드일 경우에 단계를 잘게 쪼개서 TDD로 문제없이 개발하는 능력을 갖출 수 있다. 가짜 구현으로 개발하면 다음과 같은 2가지 효과를 얻을 수 있다.
- 심리학적: 빨간 막대와 초록 막대 상태는 완전히 다르다. 막대가 초록색이라면 어느 위치인지 알고 거기부터 리팩토링해 갈 수 있다.
- 범위 조절: 하나의 구체적인 예에서 일반화를 함으로써, 불필요한 고민으로 혼동되는 일을 예방할 수 있다.
삼각 측량
삼각 측량은 테스트 주도로 추상화된 과정을 일반화하는 과정이다. 삼각 측량 방법은 테스트 예시가 2개 이상일 때에만 추상화를 해야 한다.
@Test
public void 곱하기테스트() {
// given
// when
final int result1 = multiply(2, 3);
final int result2 = multiply(4, 7);
// then
assertThat(result1).isEqualTo(6);
assertThat(result2).isEqualTo(28);
}
명백하게 구현하기
명백하게 구현하는 방법은 가짜 구현이나 삼각 측량 방법을 사용하지 않고 바로 정답을 구현하는 방법이다.
현재 사용하는 곱하기와 같은 문제는 쉬우므로 다음과 같이 바로 진짜 구현을 해도 괜찮다.
public int multiply(final int num1, final int num2) {
return num1 * num2;
}
가짜로 구현하기와 삼각측량은 매우 작은 발걸음이다. 그러므로 무엇을 타이핑해야 할지 알고, 그걸 재빨리 할 수 있다면 그냥 구현해버리는 것이 좋다. 그러다가 만약 손가락이 머리를 따라오지 못하기 시작하면 다른 구현 기법을 사용하도록 하자.
[ Spring에서의 TDD(Test-Driven Development, 테스트 주도 개발) 프로그래밍 방법 ]
- Repository -> Service -> Controller 순서로 개발을 진행한다.
- Repository 계층의 테스트는 H2와 같은 인메모리 데이터베이스 기반의 통합 테스트로 진행한다.
- Service 계층의 테스트는 Mockito를 사용해 Repository 계층을 Mock하여 진행한다.
- Controller 계층의 테스트는 SpringTest의 MockMvc를 사용해 진행한다.
다른 예제나 강의들을 보면 Controller -> Service -> Repository 순서로 TDD 개발을 하는 경우도 많이 있다. 하지만 개인적인 경험을 바탕으로 Repository부터 계층을 작성해야 TDD의 flow가 매끄럽게 진행이 되는 것 같다. 왜냐하면 Repository 계층은 다른 계층에 대한 의존성이 거의 없기 때문에 먼저 작성하기가 편리하기 때문이다. Service 계층은 Repository에 의존하기 때문에 Repository가 있어야 개발이 편리하며 흐름이 끊기지 않고, Controller 계층은 마찬가지로 Service계층에 의존하기 때문이다.
반대로 Controller부터 개발을 진행하면 Controller를 작성하다가 Service를 작성하고 그러다가 Repository 까지 내려가서 흐름이 끊길 수 있기 때문이다. 만약 Controller부터 하는게 편하다면 Controller부터 개발해도 전혀 문제가 없다. 개인에 맞게 맞춰나가면 된다.
하지만 이러한 내용을 숙지했다고 하더라도 실제로 TDD를 적용하는 것은 쉽지 않다. 그러므로 요구사항이 주어진 과제를 테스트에 의해 주도되는 방법으로 실제 개발을 진행해보고자 한다. 단순히 TDD로 개발을 하는 것 뿐만 아니라 실무에 적용할만한 알짜 지식들 또는 팁 등도 작성해보고자 한다. 많은 사람들에게 도움이 되기를 바라면서 주어진 과제를 분석하고, TDD 기반으로 개발해보도록 하자.
(만약 단위 테스트에 대한 개념이나 Java에서 단위 테스트의 작성 또는 Spring에서의 단위 테스트 작성 및 Mockito 사용법에 대한 공부가 부족하다면 여기를 참고해주세요!)
위에서 작성한 내용들은 클린코드나 이펙티브 자바, 테스트 주도 개발 By Example 등의 책과 실무 경험 등을 종합하여 개인적인 생각을 정리한 글입니다. 옳고 그른 얘기들 및 부족한 점 충분히 있을 수 있는데, 피드백이나 커멘트 등은 언제나 환영합니다:)
코드는 깃허브에 공개되어 있습니다! 코드를 확인하시려면 여기를 참고해주세요:)
관련 포스팅
- 단위 테스트와 TDD(테스트 주도 개발) 프로그래밍 방법 소개 - (1/5)
- TDD 연습문제 소개와 요구사항 분석 및 SpringBoot 프로젝트 설정 - (2/5)
- TDD로 멤버십 등록 API 구현 예제 - (3/5)
- TDD로 멤버십 전체/상세 조회 API 구현 예제 - (4/5)
- TDD로 멤버십 삭제 및 포인트 적립 API 구현 예제 - (5/5)
'나의 공부방' 카테고리의 다른 글
[OOP] 객체지향 프로그래밍의 5가지 설계 원칙, 실무 코드로 살펴보는 SOLID (38) | 2021.11.02 |
---|---|
[개발서적] 클린 코더(Clean Coder) 핵심 요약 및 정리 (10) | 2021.09.04 |
[클린코드] 변경을 최소화하는 개발, 관심사의 분리와 변하는 것과 변하지 않는 것의 분리 (0) | 2021.06.30 |
[디자인 패턴] 싱글톤이 안티 패턴이 될 수 있는 이유와 자바 싱글톤과 스프링 싱글톤의 차이 (6) | 2021.05.14 |
[클린코드] 좋은 코드는 어떤 코드일까? 좋은 코드에 대한 고찰 (6) | 2021.05.05 |