Intro
안녕하세요. 환이s입니다👋
오늘은 실무에서 개발팀이 소프트웨어 품질을 높이고, 효율적인 개발 프로세스를 유지하기 위해 소프트웨어 개발의 여러 도전 과제를 해결하는 데 효과적인 방법론인 TDD에 대해 포스팅해보려 합니다.😉
TDD는 저도 실무에서 자주 도입하려고 하는 방법론으로, 처음 접하게 된 계기는 저를 개발자로 이끌어준 지인이 했던 말이 계속 머릿속에 남아서 연습하게 되었기 때문입니다.
그 지인이 저한테 했던 말은 "TDD는 꼭 해봐라. 어렵더라도 반복 숙달을 통해 몸에 익혀라."라는 것이었습니다. 그래서 저는 이 말을 중요하게 생각하고 반복 연습을 해왔는데요🙂
저도 TDD를 알아보시는 분들을 위해 포스팅을 통해 정보를 공유해보겠습니다❗
TDD(Test-Driven Development) : 개념
TDD란 Test-Driven Development의 약자로 '테스트 주도 개발'이라고 합니다.
반복 테스트를 이용한 소프트웨어 방법론으로, 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 구현합니다.
또한, 짧은 개발 주기의 반복에 의존하는 개발 프로세스이며, Agile 방법론 중 하나인 eXtreme Programming(XP)의 'Test-First' 개념에 기반한 단순한 설계를 중요시합니다.
Agile 방법론?
Agile은 기민한, 날렵한이란 뜻으로
좋은 것을 빠르게 취하고, 낭비 없게 만드는 다양한 방법론을 통칭해 일컫는 말입니다.
앞을 예측하며 개발하지 않고,
일정한 주기를 가지고 계속 검토해 나가며 필요할 때마다
요구사항을 더하고 수정하여 커다랗게 살을 붙이면서 개발하는 프로세스 모델 방식입니다.
Agile 방법론은
계획 ➡︎ 설계(디자인) ➡︎ 개발(발전) ➡︎ 테스트 ➡︎ 검토(피드백)
순으로 반복적으로 진행됩니다.
eXtreme Programming(XP)?
eXtreme Programming. 줄여서 XP는
수시로 발생하는 고객의 요구사항에 유연하게 대응하기 위해
고객의 참여와 개발 과정의 반복을 극대화하여 개발 생산성을 향상시키는 기법입니다.
짧고 반복적인 개발 주기, 단순한 설계, 고객의 적극적인 참여로
소프트웨어를 빠르게 개발하는 것이 목적이며
이 기법은 추가 요구사항이 생기더라도, 실시간으로 반영할 수 있습니다.
Test-First?
Test-First Development는 System을 구현하기 전에,
테스트 프로그램을 먼저 개발하는 것을 의미합니다.
테스트 프로그램을 먼저 개발하며 요구사항에 대한 이해도를 더 높일 수 있고,
새로운 Release가 빌드될 때마다 모든 Component에 대한 테스트를 진행합니다.
즉, 먼저 개발된 Component들은 누적하여 테스팅되기 때문에 신뢰도가 더욱 상승하며,
개발팀에 속한 사용자들이 .Acceptance Test(Alpha Test)에 도움을 줍니다.
하지만 TDD, 즉 테스트 주도 개발(Test-Driven Development)에 대한 프로그래머들의 의견은 늘 엇갈립니다.
TDD의 실효성을 업무로 경험한 사람들은 TDD를 더 효과적으로 실무에 적용하기 위해 고민합니다.
반면, 회사마다 일하는 방식이나 처한 업무 환경에 편차가 있다 보니 일각에서는 실무에서 TDD를 사용하는 건 사실상 현실과 괴리감이 크다는 의견도 있습니다.
TDD(Test-Driven Development) : 개발주기
위 이미지는 'TDD 개발주기'를 표현한 것입니다.
- [RED] : 실패하는 테스트 코드를 먼저 작성합니다.
- TDD 개발에서의 첫 번째 단계는 실패입니다.
- 즉, 실패하는 테스트 케이스를 작성하는 것입니다. 이때 테스트 케이스는 프로젝트 전체의 모든 기능에 대해서 모든 케이스를 작성하는 것이 아니라, 먼저 구현할 기능에 대해서 테스트 케이스를 하나씩 작성하는 것입니다.
- [Green] : 테스트 코드를 성공시키기 위한 실제 코드를 작성합니다.
- 두 번째 단계는 성공입니다.
- 실패 단계에서 작성한 테스트 케이스를 통과시키기 위한 최소한의 코드를 작성하는 단계입니다.
- [Blue] : 중복 코드 제거, 일반화 등의 리팩토링을 수행합니다.
- 세 번째 단계는 리팩토링 단계입니다.
- 성공 단계에서 작성한 코드에 중복되는 코드나, 개선시킨 방법이 있다면 리팩토링을 진행해 줍니다.
중요한 것은 실패하는 테스트 코드를 작성할 때까지 실제 코드를 작성하지 않는 것과, 실패하는 테스트를 통과할 정도의 최소 실제 코드를 작성해야 하는 것입니다.
이를 통해, 실제 코드에 대해 기대되는 바를 보다 명확하게 정의함으로써
불필요한 설계를 피할 수 있고, 정확한 요구 사항에 집중할 수 있습니다.
TDD(Test-Driven Development) : 프로그래밍 방법
지금까지 개념에 대해 알아봤습니다.
그렇다면 제가 실무에서 TDD를 적용한 사례 기반으로 예제 코드를 생성해서 알아보겠습니다.
저는 Todo List 애플리케이션을 TDD 방식으로 구현했었는데, 실무에서 사용했던 방식 일부를 기반으로 단계별로 진행해 보겠습니다.
이 예제에서는 기본적인 기능인 할 일 추가, 삭제, 조회를 구현하겠습니다.
1. 요구사항 정의
- 할 일 추가 : 새로운 할 일을 추가할 수 있어야 한다.
- 할 일 삭제 : 특정할 일을 삭제할 수 있어야 한다.
- 할 일 조회 : 모든 할 일을 조회할 수 있어야 한다.
2. 테스트 작성 (Red 단계)
- 목적 : 테스트를 작성하여 기능의 요구사항을 명확히 하고, 그 테스트가 실패하는 것을 확인합니다.
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
public class TodoListTest {
private TodoList todoList;
@BeforeEach
public void setUp() {
todoList = new TodoList();
}
@Test
public void testAddTodo() {
todoList.add("Buy groceries");
assertEquals(1, todoList.getAll().size());
}
@Test
public void testRemoveTodo() {
todoList.add("Buy groceries");
todoList.remove("Buy groceries");
assertEquals(0, todoList.getAll().size());
}
@Test
public void testGetAllTodos() {
todoList.add("Buy groceries");
todoList.add("Walk the dog");
List<String> todos = todoList.getAll();
assertEquals(2, todos.size());
assertTrue(todos.contains("Buy groceries"));
assertTrue(todos.contains("Walk the dog"));
}
}
위 코드를 보면 'TodoList' 클래스의 기능을 테스트하기 위해
'TodoListTest' 클래스에 여러 테스트 메서드를 작성했습니다.
여기서는 'testAddTodo', 'testRemoveTodo', 'testGetAllTodos' 메서드가 있는데
각각 할 일 추가, 삭제, 조회하는 기능을 검증합니다.
@Test
public void testAddTodo() {
todoList.add("Buy groceries");
assertEquals(1, todoList.getAll().size()); // 이 테스트는 실패해야 함
}
위의 테스트를 실행하면 'TodoList' 클래스가 아직 구현되지 않았거나,
기능이 잘못 구현되어 있기 때문에 테스트가 실패합니다. 이는 TDD에서 정상적인 과정입니다.
실패하는 테스트는 개발자가 어떤 기능을 구현해야 하는지를 명확하게 알려줍니다.
3. 코드 구현 및 테스트 통과 (Green 단계)
- 목적 : 작성한 테스트를 통과시키기 위해 필요한 최소한의 코드를 구현합니다.
import java.util.ArrayList;
import java.util.List;
public class TodoList {
private List<String> todos;
public TodoList() {
this.todos = new ArrayList<>();
}
public void add(String todo) {
todos.add(todo);
}
public void remove(String todo) {
todos.remove(todo);
}
public List<String> getAll() {
return new ArrayList<>(todos);
}
}
'TodoList' 클래스와 'add', 'remove', 'getAll' 메서드를 작성하여 테스트가 통과하도록 합니다.
이 단계에서는 간단한 코드를 작성하여 테스트를 통과시키는 것이 목표이며, 작성한 코드를 실행하여 모든 테스트가 통과하는지 확인합니다.
모든 테스트가 성공하면, 해당 기능이 제대로 구현되었다고 볼 수 있는데,
앞서 실패한 'testAddTodo'를 실행해서 성공적으로 통과하면, 할 일이 추가되었음을 확인할 수 있습니다.
4. 코드 리팩토링(Refactor 단계)
- 목적 : 테스트가 통과한 후, 코드를 개선하고 정리합니다.
리팩토링 단계에서는 코드의 가독성, 유지보수성, 성능 등을 개선합니다.
이 과정에서는 알고리즘을 최적화하거나, 중복 코드를 제거하고, 코드 구조를 정리하는데
예를 들어, remove 메서드에서 할 일이 존재하지 않을 때의 예외 처리를 추가할 수 있습니다.
public void remove(String todo) {
if (!todos.contains(todo)) {
throw new IllegalArgumentException("Todo item not found: " + todo);
}
todos.remove(todo);
}
여기서 테스트 재검증을 해봅니다.
리팩토링 후, 모든 테스트를 다시 실행하여 기존 기능이 여전히 올바르게 작동하는지 확인하고, 모든 테스트가 통과하면 리팩토링이 성공적이었다고 할 수 있습니다.
추가로, 삭제하려는 할 일이 존재하지 않을 경우를 검증하는 테스트를 작성할 수 있습니다.
@Test
public void testRemoveNonExistentTodo() {
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
todoList.remove("Non-existent todo");
});
assertEquals("Todo item not found: Non-existent todo", exception.getMessage());
}
요약해 보면
- [RED] 단계 : 실패하는 테스트를 작성하여 기능 요구사항을 명확히 합니다.
- [Green] 단계 : 테스트를 통과시키기 위한 최소한의 코드를 작성합니다.
- [Refactor] 단계 : 코드를 정리하고 개선하되, 기존의 기능이 유지되도록 합니다.
TDD(Test-Driven Development) : TDD 개발 장점 및 단점
마지막으로 TDD 개발 방식에 장점과 단점에 대해 알아보고 마무리하겠습니다.
🟩장점
1. 객체지향적인 코드 개발
테스트 코드를 먼저 작성한다면 명확한 기능과 구조를 설계할 수 있게 됩니다.
또한, 테스트의 용이성을 위한 복잡한 기능들을 하나의 함수에 모두 구현할 경우 테스트 방식이 복잡해지고 시간이 오래 걸리며 수정이 되는 경우 테스트 코드를 재 사용 할 수 없게 됩니다.
따라서 TDD 프로세스를 따르는 코드의 경우에는 재사용성을 고려하며 작성하게 됩니다.
2. 설계 수정 시간의 단축
TDD는 일반 개발 방식과는 다르게 테스트 코드를 먼저 작성하고 테스트를 진행하기 때문에 설계의 구조적인 문제를 바로 찾아낼 수 있습니다.
문제를 발견했다면 피드백을 주고받으며 수정을 하고 추가로 다시 테스트를 진행하게 됩니다.
따라서 개발이 완료된 시점에서 설계의 문제점을 발견하고 설계를 수정한 뒤 코드 전체를 수정하는 상황을 겪지 않아도 됩니다.
3. 유지 보수의 용이성
개발이 완료된 시점에서 어떠한 기능을 추가하거나 수정하는 경우에 가장 고려되는 부분이 수정하거나 추가한 코드가 전체 프로그램에 어떠한 영향을 미칠지 모른다는 점입니다.
단순한 기능의 수정 혹은 추가도 전체 프로그램에 큰 영향을 끼칠 수가 있고 이러한 경우 모든 기능들을 처음부터 다시 테스트를 진행해야 합니다.
그러나 TDD 프로세스에 경우 자동화된 유닛 테스팅을 전제하므로 이렇게 전체를 테스트하는 시간을 단축시킬 수 있습니다.
4. 디버깅 시간의 단축
TDD 프로세스는 단위 테스트 기반으로 진행되기 때문에, 프로그램에 문제가 발생하였을 때에도 각각의 모듈 별로 테스트를 진행해 보면 문제를 쉽게 찾아낼 수 있습니다.
만약 일반 개발 방식이었다면, 버그를 찾기 위해서 프로그램 전체 코드를 봐야 할 수도 있는데, 프로그램이 큰 경우 여러 영역에 걸쳐 전체 코드를 검토하는 것이 쉽지 않습니다.
🟥단점
1. 초기 개발 속도 저하
TDD는 "테스트를 먼저 작성하고 그 테스트를 통과시키기 위한 코드를 작성한다."는 원칙에 기반합니다.
이 과정에서 개발자는 기능을 구현하기 전에 테스트 케이스를 정의해야 하므로, 초기 개발 속도가 느려질 수 있습니다.
예를 들어, 특정 기능을 구현하기 위해 여러 테스트 케이스를 작성해야 할 때,
개발자는 해당 기능에 대한 요구사항을 명확히 이해하고, 그에 맞는 테스트를 설계해야 합니다.
이 과정에서 많은 시간을 소요할 수 있고, 특히 TDD에 익숙하지 않은 개발자라면 더 많은 시간과 노력이 필요할 수 있습니다..💦
2. 복잡한 테스트 관리
TDD를 통해 작성된 테스트 케이스가 많아지면, 이들을 관리하는 데 어려움이 생길 수 있는데
특히, 프로젝트가 커지고 기능이 추가되면서 테스트의 수가 늘어나면, 테스트 관리가 복잡해질 수 있습니다.
예를 들어, 여러 개발자가 동시에 작업하는 대규모 프로젝트에서는 팀원 각각이 작성한 테스트 케이스가 서로 다를 수 있습니다.
이 경우, 테스트의 일관성을 유지하고, 수정 사항을 반영하는 것은 어렵습니다. 또한, 코드 변경 시 관련된 테스트를 찾고 수정하는 데 시간이 오래 걸립니다.. 💦
3. 상황에 따라 비효율적일 수 있습니다.
TDD는 모든 상황에 적합한 방법론이 아닙니다.
특히, 제가 최근에 끝낸 공공기관 프로젝트처럼 개발 기간이 적은 프로젝트나 소규모 프로젝트 같은 경우 TDD가 오히려 비효율적일 수 있습니다. 이 경우, 테스트를 작성하는 데 소비되는 시간과 노력이 실제로는 필요 없는 경우가 많습니다.
조금 더 설명해 보면,
간단한 스크립트나 프로토타입을 작성할 때는 빠른 개발이 더 중요할 수 있는데, 이때 TDD를 적용하여 테스트를 작성하는 것이 오히려 개발 속도를 저하시키고, 필요 없는 작업이 될 수 있습니다. 또한, 테스트가 필요 없는 경우에는 테스트 작성이 불필요한 부담이 될 수 있습니다.
이러한 장점 및 단점들을 통해 TDD를 적용할 때, 항상 최선의 선택은 아닌 걸 생각하고 팀의 상황과 프로젝트의 요구사항에 따라 적절하게 적용하면 될 거 같습니다.
마치며
오늘은 TDD에 대한 개념 및 프로그래밍 방법에 대해 알아봤는데요.
단점에서 언급했듯 TDD는 익숙하지 않은 개발자라면 더 많은 시간과 노력이 필요합니다.
저도 실무에서 사용하기까지 많은 시간이 투자되고.. 실제로 TDD를 사용하지 않고 진행해야 하는 프로젝트가 더 많다 보니 자기 계발을 하면서 계속 다듬고 있는 단계입니다😅
다음 포스팅에서 뵙겠습니다👋
'[ Concept ]' 카테고리의 다른 글
[ Concept ] 효과적인 백엔드 개발 - 성능최적화 전략 알아보기 (1) | 2024.12.24 |
---|---|
[ Concept ] 프로젝트 산출물 - 화면설계서 (3) | 2024.12.04 |
[ Concept ] HTTP와 HTTPS의 차이점 (0) | 2024.08.20 |
[ Concept ] SSO(Single Sign-On) 통합 인증 알아가기 (0) | 2024.07.11 |
[ Concept ] 분산 코디네이터 Zookeeper(주키퍼) 알아가기 (0) | 2024.05.20 |