개발을 하다보면 자주 보는 단어가 하나있다.
그것은 바로 TDD.
자주 본 단어기는 한데, 잘 와닿지는 않는다. 방대한 레거시를 가지고 있고, 고객 요구사항을 빨리 쳐내야하는 SM 중심의 회사일 수록 TDD와는 거리가 먼 삶을 살게 된다. (like me..)
그러던 중 좋은 기회로 TDD로 간단한 회원 기능을 구현해볼 일이 생겨, 구현 과정에서 배우고 느낀 점을 남겨보려고 한다.
# 그래서 TDD가 뭔데?
일단 용어부터 정의를 하고 넘어가보자. TDD란 무엇인가?
Test-Driven Development 의 약자로 테스트 주도 개발을 뜻한다.
테스트 주도? 테스트 안 하는 개발자가 어딨어? 라고 생각할 수 있겠지만 선 개발 후 테스트가 아니라 선 테스트 후 개발이다.
일단 실패하는 테스트를 만들고 (Red Phase), 테스트가 통과할 수 있도록 구현을 하고 (Green Phase), 구현된 코드를 고도화/리팩터링 (Blue Phase - Refactor Phase라고 하기도 한다) 한다.
이때 실패하는 테스트는 정확히 무엇을 뜻하는 걸까?
간단한 회원 가입을 예로 들어보자면, 회원은 아이디와 이름을 가지며 극단적으로 아이디는 영어만, 이름은 한글만 입력 가능하다는 제약 조건이 있다. 이때 우리가 회원 가입을 구현하기 위해 가장 먼저 하는 것은 무엇일까?
사람마다 다르겠지만, 일단은 값을 저장할 회원 모델을 어떻게 구성할지 생각해보는 거다. 모델의 이름은 뭘로 하지? 변수명은?
그리고 생각한 모델 생성 코드를 테스트 코드에 일단 적어본다. 실제로 회원 모델은 정의되어 있지 않으니 테스트는 당연히 빨간줄이 가득하고 실행하면 결과창에 아주 새빨간불이 뜨게 된다.
이것이 바로 Red Phase. 실패하는 테스트이다.
이제 테스트 결과를 초록 불로 바꾸기 위해 회원 모델을 만들고, 제약 조건들을 구현하며 Green - Blue Phase를 지나게 되면 완벽(?)한 코드가 완성이 되는 것이다!
Red-Green-Blue는 이제 완벽하게 이해했으나, 그럼 이 테스트를 얼마나 자세히 쪼개야하는 것이며 어디선가 들어본 단위테스트, 통합테스트는 무슨 기준으로 나눠야하는 건데? 라는 의문이 자연스럽게 떠오른다. 나는 이 부분에서 자연스럽게 DDD(도메인 주도 설계)가 함께 녹아들게 된다는 생각이 들었다.
# TDD와 DDD
우리가 가장 중요하게 생각하는 개념을 기준으로 비즈니스 로직에 맞는 'Domain'을 만든다. 이 도메인에 대한 테스트를 작성하게 되면 외부 의존성이 없는 '단위 테스트'가 된다.
이후, 도메인을 가지고 실제 서비스단의 로직(DB 저장, 혹은 다른 도메인과 협력 등)을 생각하며 테스트를 작성한다. mock과 stub 모의 객체를 생성하고 정의하여 외부 의존성을 대체하거나, @SpringBootTest 어노테이션을 이용해서 직접 서버를 띄워서 '통합 테스트'를 진행하게 되는 것이다.
예를 들어 헤더에 회원 인증 값이 있는지에 대한 검증이 필요하다면, 일단 테스트 코드를 만든다. mockMvc를 통한 통합 테스트 코드를 작성하거나, 실제 서버를 띄워 HTTP 호출을 할 수 있도록 'E2E 테스트' 코드를 작성하는 것이다.
그리고 테스트 코드가 동작할 수 있도록 구현 방향을 정하는 것이다. 이것을 Filter에서 처리할까? Interceptor에서 처리할까? 이걸 추가하면 다른 테스트 코드(결국은 다른 비즈니스 로직을 가리킨다고 생각한다.)에는 영향이 없을까?
이 상황에서 나는 Interceptor에서 처리하는 것을 택했다. Filter에서 처리하게 되면 Controller로 들어오는 요청 뿐만 아니라 다양한 경로까지 로직을 처리할 수 있으나, @ControllerAdvice가 적용되지 않아 에러를 직접 만들어서 반환해야 한다. 현재 상황에서 수고로움을 이겨내고 HandlerExceptionResolver를 주입해서 에러 처리를 하면서까지 Filter를 사용할만한 요건이 아니라고 판단되어 Interceptor에서 처리했다.
구현을 하고 나면 테스트 코드를 돌려본다. 다른 로직에 영향이 없는지도 이미 정의해둔 테스트 코드를 실행해서 확인할 수 있다. 영향도 파악이 조금 더 쉬워진 것이다!
이렇게 TDD와 DDD를 섞어 우리가 가장 중요하게 생각하는 비지니스 도메인을 잡고 이것을 중심으로 단위 테스트부터 시작해 테스트 - 구현을 반복하며 완전한 로직 구현으로 점차 확장해 나갈 수 있다.
지금까지만 보면 정말 완벽해보인다. 돌아가는 쓰레기도 기도메타도 없는 행복한 개발 세상이 될 것 같다.
그렇다면 TDD는 정말 완벽한 방법론인걸까?
# 비즈니스 로직은 내가 생각할게 테스트는 누가 짤래?
좋은 테스트란 무엇일까? [F]ast, [I]solated, [R]epeatable.... 같은 이야기는 일단 차치하고, 개인적으로는 비즈니스 로직을 잘 담고 있고, 테스트 시나리오가 잘 정리된 테스트라고 생각한다.
하지만 비즈니스 로직에 대한 촘촘한 검증을 하고자 한다면, 수많은 테스트 케이스를 만들어야 할텐데 이 테스트들은 대체 누가 다 짜야하는 것일까? 단위 테스트는 그렇다쳐도 통합테스트, E2E 테스트까지 생각하면 머리가 지끈거린다.
하지만 지금이 무슨 시대인가? 바로 대AI 시대이다. 정의된 비즈니스 로직에 대한 테스트 케이스를 작성하는데 AI Agent를 활용한다면 더욱 손쉽고 빠르게 개발을 할 수 있는 것이다.
단위 테스트 작성 시, AI에게 미리 정의해둔 비즈니스 로직과 클래스 명들을 알려주고 제약 사항을 전달한다. 완성된 테스트 케이스들을 검증하고, 이에 맞게 실제 개발을 진행한다. 통합 테스트 케이스 작성 시에는 단위 테스트 케이스 제약 사항들을 참고하여 추가적인 비즈니스 로직을 담아 테스트 케이스를 짜달라고 명령을 한다. 이미 정의 및 통과된 테스트 케이스들이 있기 때문에 이를 기반으로 빠르고 정확(?)한 케이스들을 생성해준다.
이렇게 생성된 수많은 케이스들을 적절히 관리하는 방법도 중요할 것이다. 동일한 테스트 케이스에서 여러 parameter로 테스트해보고 싶은 경우에는 `@ParameterizedTest`를 사용하고, 논리적으로 관련된 테스트들을 `@Nested`로 묶어 구조화하고 가독성을 높일 수 있었다.
기능 구현을 하며 느낀 것이지만 AI와 TDD는 상성이 아주 좋다. A 라는 기능을 개발할 때 테스트 케이스 없이 AI에게 코드 구현을 요청하면 요구사항에 없던 엉뚱한 로직까지 구현할 때가 많다. 하지만 이미 정의된 테스트 케이스를 기반으로 테스트를 통과하는 코드 구현 요청을 하면, 대체로 요구한 코드만 생성하고 디버깅 하는 시간도 줄어드는 걸 체감했다.
AI와 함께 개발(속된 말로 '딸깍')을 하는 일이 점점 많아지는 이 시대에 TDD는 안전한 코드를 빠르게 생성하게 해주는 장치라고 생각한다. 이를 증명하듯, SDD(Spec-Drvien Development)라는 명세 주도 개발이 떠오르고 있는데, TDD는 필요한 구현 내용과 제약 사항을 테스트로 작성했고 SDD는 말그대로 '스펙', 즉 사용자 스토리와 아키텍처-구현 방법등의 명세를 작성하고 이를 AI에게 학습시켜 개발을 진행하는 것이다. 이는 바이브 코딩의 문제점(어느 순간이 지나면 AI가 생성된 코드와 요구사항을 기억하지 못한다.)을 해결할 수 있다는 것이다.
# 그래서 TDD는 정말 효율적인가?
혹자들은 'TDD는 죽었다' 거나 '사양된 방법론' 이라고 얘기를 한다. 수많은 테스트 케이스를 작성해야 하고 이를 유지보수하는 비용이 매우 크기 때문일 것이다.
AI를 도입하여 개발하는 상황이 많아지는 지금 TDD는 효율을 따지기 보다 AI를 이용해 안전하고 빠르게 원하는 기능을 구현하기 위한 도구라고 생각한다. SDD, TDD, DDD를 적절하게 활용한다면 고품질 코드 생산성을 높일 수 있지 않을까 싶다.
간단한 회원 기능 구현이었지만 TDD에 대해 여러 생각을 해볼 수 있는 좋은 경험이었다.
'이것저것' 카테고리의 다른 글
| 루퍼스 백엔드 4기를 끝내며 (1) | 2026.08.02 |
|---|---|
| [Spring boot] Postgresql 연결 (0) | 2022.05.20 |
| Vuejs 시작 명령어 (0) | 2022.05.10 |
| 클라우드란? (0) | 2022.01.06 |
| Git Hub 프로필 꾸미기 (0) | 2021.09.25 |