개발자라면 누구나 한 번쯤 TDD(Test-Driven Development) 테스트 주도 개발에 대해 들어보았을 것이다. 이번 포스팅에서는 TDD에 대해서 다룬다.
1. 문제 상황 : 테스트 코드를 나중에 작성하면 생기는 일
보통의 개발 흐름은 요구사항을 받으면 일단 비즈니스 로직부터 작성한다. 기능이 정상 동작하는 것을 확인한 후 뒤늦게 테스트 코드를 덮어 씌운다 (이를 Test-Last 방식이라 한다.)
Bad Code: 테스트하기 어려운 설계
주말에는 영화 티켓값을 10% 할인해 주는 로직을 개발했다고 가정하자
public class TicketService {
// [Bad Code] 운영 코드를 먼저 짜면, 현재 시간에 강하게 결합된 코드가 나온다.
public int calculatePrice(int basePrice) {
DayOfWeek today = LocalDate.now().getDayOfWeek(); // 제어할 수 없는 외부 상태(현재 시간)에 의존
if (today == DayOfWeek.SATURDAY || today == DayOfWeek.SUNDAY) {
return (int) (basePrice * 0.9);
}
return basePrice;
}
}
이제 이 코드를 검증하기 위해 테스트 코드를 작성한다고 했을 때, 이 테스트는 오늘이 무슨 요일이냐에 따라 성공과 실패가 오락가락 하는 엉터리 테스트가 된다. 수요일에 이 코드를 커밋하고 CI/CD를 돌리면 주말 할인 테스트는 무조건 실패한다.
2. TDD란? (Red-Green-Refactor)
TDD는 개발의 순서를 완전히 뒤집는다. 실패하는 테스트 코드를 먼저 작성하고(Red), 그 테스트를 통과시킬 만큼만 코드를 구현한 뒤(Green), 코드를 깔끔하게 다듬는(Refactor) 과정을 짧게 반복한다.
RED (실패하는 테스트)
아직 구현되지 않은 기능의 테스트를 작성한다. 이때 어떻게 구현할지가 아니라 무엇을 해야 하는지(인터페이스와 스팩)를 먼저 고민한다.
GREEN (테스트 통과)
꼼수를 써서라도 가장 빠르게 테스트를 통과 시킨다.
REFACTOR(리팩토링)
테스트가 코드를 보호해주고 있으므로, 안심하고 중복을 제거하고 객체 지향적인 설계로 다듬는다.
3. 실전 TDD 적용
Step 1: RED (테스트를 먼저 설계한다.)
할인 로직을 검증하려면, 내가 요일을 임의로 조작할 수 있어야 한다는 사실을 테스트 코드를 짜면서 자연스럽게 깨닫게 된다.
class TicketServiceTest {
@Test
@DisplayName("주말(토, 일)에는 티켓 가격이 10% 할인되어야 한다.")
void weekend_discount() {
TicketService service = new TicketService();
// given: 내가 직접 날짜를 주입(제어)할 수 있도록 인터페이스를 설계하게 됨!
LocalDate saturday = LocalDate.of(2023, 10, 28);
// when
int price = service.calculatePrice(10000, saturday);
// then
assertThat(price).isEqualTo(9000);
}
}
코드 구현이 없으므로 당연히 컴파일 에러가 나거나 테스트에 실패한다.
Step 2: GREEN (가장 단순한 구현)
테스트를 통과시키는 것이 최우선이다. 오직 테스트를 만족시키는 코드만 작성한다.
public class TicketService {
// 테스트 코드를 통해 '날짜'를 외부에서 주입받아야 한다는 설계를 이끌어냈다!
public int calculatePrice(int basePrice, LocalDate date) {
if (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY) {
return (int) (basePrice * 0.9);
}
return basePrice;
}
}
Step 3: REFACTOR (안전한 개선)
이제 테스트가 든든하게 받쳐주고 있으니, 로직을 더 우아하게 개선해 본다.
public class TicketService {
public int calculatePrice(int basePrice, LocalDate date) {
if (isWeekend(date)) {
return (int) (basePrice * 0.9);
}
return basePrice;
}
// 주말 여부를 판단하는 로직을 별도 메서드로 추출 (가독성 향상)
private boolean isWeekend(LocalDate date) {
DayOfWeek day = date.getDayOfWeek();
return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY;
}
}
4. TDD의 가치: 테스트가 아닌 설계(Design)을 주도한다.
TDD의 목적은 단순히 커버리지 100%의 방대한 테스트 코드를 짜는 것이 아니다. 앞선 예제에서 보았듯, 사용자(클라이언트)의 관점에서 테스트를 먼저 작성하다 보면, 필연적으로 테스트하기 좋은 코드(= 의존성이 낮고 모듈화가 잘 된 결합도 낮은 코드)를 강제로 설계하게 된다.
- LocalDate.now() 같은 숨겨진 의존성은 메서드 파라미터나 의존성 주입(DI)으로 끌어내게 된다.
- 비대한 Service 클래스는 테스트가 너무 복잡해지기 떄문에, 자연스럽게 역할을 분리(Entity 나 다른객체로 위임)하게 된다.
결론
바빠서 테스트 코드를 짤 시간이 없다는 말은 사실 모순이다.
개발 이후에 손으로 Postman을 수십 번 클릭하며 디버깅하는 시간, 라이브 서버에서 터진 버그를 수습하느라 날리는 주말을 생각하면 TDD는 시간을 잡아먹는 것이 아니라 오히려 시간을 가장 확실하게 아껴주는 투자다.
복잡한 비즈니스 로직이나 도메인 핵심 규칙만큼은 반드시 테스트를 먼저 작성하는 TDD 사이클을 경험하는 습관을 들이자
'Software Engineering' 카테고리의 다른 글
| [Software Enginnering] 에자일(Agile) 이란? (0) | 2026.04.16 |
|---|---|
| [Software Enginnering] 워터폴(Waterfall) 방법론의 명과 암 (0) | 2026.04.13 |
| [Clean Code] 실무에서 알아야할 5가지 네이밍 컨벤션과 데이터 매핑 전략 (0) | 2026.04.07 |
| [Software Engineering] 트랜잭션 스크립트 패턴(Transaction Script Pattern) (0) | 2026.03.31 |
| [Software Engineering] DDD(도메인 주도 설계)란 (0) | 2026.03.26 |
