
우테코 미션 1을 진행하며 직접 경험하고 이해한 TDD와 페어 프로그래밍에 대한 회고를 정리해 보고자 한다.
핵심 로직부터 시작해 입출력 레이어까지 단위 테스트를 잘게 쪼개어 구현하고, 마침내 컨트롤러를 통한 통합 테스트까지 성공시킨 일련의 과정은 개인적으로 무척 뜻깊은 경험이었다.
단위 테스트부터 통합 테스트까지 범위를 넓혀가며 느낀 점은, 개발 후반부로 갈수록 미리 작성해 둔 테스트 코드 하나하나가 든든한 버팀목이 된다는 것이다. 기능을 추가하거나 리팩터링을 할 때 기존 테스트가 모두 통과하면 기존의 외부 동작이 훼손되지 않았다는 확신을 가질 수 있었다. 반대로 예상치 못한 변경으로 인해 테스트가 실패하더라도, 정확히 어느 단위에서 문제가 발생했는지 즉각적으로 파악할 수 있다는 점이 TDD의 가장 확실한 장점임을 깨달았다.
TDD란 무엇일까?
TDD, 즉 테스트 주도 개발은
"코드를 먼저 작성하는 것이 아니라, 테스트를 먼저 작성하고 그 테스트를 통과하는 코드를 이후에 작성하는 개발 방법론"이다.
일반적인 개발 순서가 "구현 -> 테스트"라면, TDD는 "테스트 -> 구현 -> 리팩터링"의 순서를 따른다고 보면된다.
이 방식이 갖는 핵심 가치는 "설계를 테스트가 이끈다"라는 점에 있다. 그리고 필자는 전적으로 이 말에 동의한다.
쉽게 생각해보자. 테스트를 먼저 작성한다는 것은, 내가 만들 코드의 인터페이스와 동작을 먼저 정의한다는 의미이다.
그러니 구현 이전에 설계를 자연스럽게 강제하게 되지 않을까?
필자가 경험한 TDD의 장점은 크게 아래와 같다.
1. 복잡한 비즈니스 로직을 작은 단위로 쪼개어 검증하면서 점진적으로 완성도를 높일 수 있다.
2. 테스트가 곧 문서 역할을 하기 때문에, 코드를 처음 보는 사람도 테스트 코드를 통해 해당 코드가 어떤 동작을 해야 하는지 빠르게 이해할 수 있다.
이 외에도 요구사항이 자주 변경되는 프로젝트에서 변경 사항이 기존 기능을 망가뜨리는지 즉각적으로 검증할 수 있다는 장점도 있다. (개인적으로프로젝트 요구사항, 즉 미션요구사항이 필자의 기준에서 변경되지는 않았지만 리팩터링 시에 코드의 변경 사항이 기존 기능을 망가뜨리는지 즉각적으로 검증했던 점은 효율적인 개발로 이끈 것 같다.)
번개 메타포 (핵심부터 브랜치로 ⚡️)
강의 중에 준이 설명해주신 메타포가 너무 인상적이었다.
TDD를 처음 접했을 때 가장 어려운 질문은 "어디서부터 테스트를 시작해야 하는가"이다.
이 고민에 대한 답은 번개 메타포이다.
번개는 구름에서 땅으로 떨어질 때 하나의 굵은 줄기가 먼저 내리꽂히고, 그 이후에 가느다란 브랜치들이 퍼져나간다.
TDD도 이와 같다.
가장 핵심적이고 가장 작은 단위의 기능에 먼저 번개를 꽂고, 그로부터 파생되는 부수적인 기능들을 브랜치처럼 뻗어나가며 테스트한다.
로또 미션을 진행할 때, 처음에는 번개를 어디에 꽂아야 할지 막막했다.
입력 처리부터 시작해야 할지, 당첨 판정 로직부터 시작해야 할지 갈피를 잡기 어려웠다.
여러 고민 끝에 '비교'라는 기능이 이 시스템의 가장 핵심적인 중심 기능이라는 결론을 내렸다.
사용자가 구매한 로또 번호와 당첨 번호를 비교하는 것 자체가 로또 게임의 본질이기 때문이다.
그래서 '비교' 기능에 번개를 꽂은 뒤, 수익률 계산, 당첨 통계, 번호 생성, 그리고 마지막으로 입출력의 작은 단위로 브랜치를 만들어가는 방식으로 접근해보기로 했다.
이렇게 핵심에서 출발해 주변으로 퍼져나가는 방식이 TDD의 흐름과 자연스럽게 맞아떨어진다는 것을 알게 되었다:)
TDD의 3단계 사이클 (Red, Green, Blue)
아래의 그래프를 보자. 아마 TDD를 공부했으면 이 그래프를 한 번쯤은 볼 수 있었을 것이다.
TDD의 핵심은 Red → Green → Blue(Refactor) 라는 세 단계의 짧은 반복 사이클이다.
각 단계의 의미를 제대로 이해하는 것이 TDD를 올바르게 실천하는 출발점이고, 직접 경험해보기 전까진 이해가 안되는점이 있었다.
한 단계씩 이해해보자.

1. Red - 실패하는 테스트 작성
Red 단계에서는 아직 구현하지 않은 기능에 대한 테스트를 먼저 작성한다.
당연히 이 테스트는 실패한다. 이 단계에서 중요한 것은 테스트가 명확한 요구사항을 표현해야 한다는 점이다.
테스트 자체가 "이 코드는 이런 동작을 해야 한다"는 명세서가 된다.
아래는 실제 미션에서 Lotto 클래스의 생성자를 검증하기 위해 작성한 테스트이다.
generateLottos()와 Lotto 클래스가 아직 존재하지 않으므로, 이 테스트를 실행하면 즉시 실패한다.
test("Lotto 생성 시 번호가 6개인 경우 -> 정상", () => {
const lottoNumbers = [[1, 2, 3, 4, 5, 6]];
expect(generateLottos(lottoNumbers)).toEqual([
new Lotto([1, 2, 3, 4, 5, 6]),
]);
});
test("Lotto 생성 시 번호가 5개인 경우 -> 에러", () => {
const lottoNumbers = [[1, 2, 3, 4, 5]];
expect(() => generateLottos(lottoNumbers)).toThrow("[ERROR]");
});
generateLottos()가 존재하지 않는 상태에서 이 테스트를 실행하면 당연히 실패한다.
테스트가 실패한다는 것 자체가, 지금 무엇을 만들어야 하는지를 명확하게 알려주는 신호이다.
처음에는 어떤 메서드, 클래스 등 뭐를 구현해야될지 감이 오지 않는다.
하지만 테스트를 작성하며 설계를 하게될텐데 이 과정이 개인적으로 TDD가 갖는 이점이 아닐까 싶다.
2. Green - 테스트를 통과하는 최소한의 코드 작성
Green 단계에서는 방금 작성한 테스트를 통과시키는 것만을 목표로 코드를 작성한다.
이 단계에서의 핵심 원칙은 테스트를 통과하는 가장 빠른 코드를 작성하는 것이다.
코드의 아름다움, 구조의 완성도는 전혀 고려하지 않아도 된다.
예를 들어보자.
import Lotto from "./Lotto.js";
export const generateLottos = (randomNumbersList) => {
return randomNumbersList.map((randomNumbers) => new Lotto(randomNumbers));
};
generateLottos 함수를 정의하고 안에서 Lotto 생성자를 통해 인스턴스를 생성하고 있는데,
관련 Lotto 클래스를 만들면된다.
사실 막 보면 더티 코드가 아닌데?라는 생각이 들 수 있다.
로또 미션을 진행하면서 가장 많이 고민했던 것은 "Green 단계에서 얼마나 더티한 코드까지 허용해야 하는가"였다.
테스트를 통과시키기 위해 하드코딩을 해도 되는 건지, 그냥 빠르게 작성하면 되는 건지 매번 경계가 모호하게 느껴졌다.
결론적으로 이해한 것은 Green 단계의 목표는 오직 해당 테스트 하나를 통과시키는 것이라는 점이다.
TDD 원리를 이해하기 위해 극단적인 예시를 살펴보자.
아래는 getRank()의 첫 번째 테스트("6개 일치 → 1등")만 통과시키기 위해 의도적으로 하드코딩한 더티한 Green 구현이다.
// "6개 일치 → 1등" 테스트 하나만 통과시키기 위한 코드
getRank(lotto) {
return 1; // 무조건 1을 반환하면 첫 테스트는 통과
}
물론 실제로 이렇게까지 작성하지는 않는 것 같다.
하지만 TDD의 원리를 이해하는 데 이 극단적인 예시가 도움이 된다고 생각한다.
이 더티한 코드는 test.each의 두 번째 케이스("5개 + 보너스 → 2등")가 실행되는 순간 즉시 깨진다.
새로운 테스트 케이스가 기존의 더티한 구현을 자연스럽게 무너뜨리고 제대로 된 구현을 강제하는 것,
바로 이것이 TDD가 올바른 설계를 이끌어내는 원리이지 않을까?
Green 단계에서 클린 코드를 작성하려는 충동 자체를 참는 것이 TDD 훈련의 핵심이고,
개인적으로 더티한 코드를 빠르게 작성하고 리팩터링하는 과정을 반복하다 보면
지금 내가 무엇을 설계하고 있는지가 오히려 더 명확하게 보이게 됬다.
이번 미션에서 직접 경험한 것은, Blue 단계에서 리팩터링을 할 때 머릿속으로 처음부터 완벽한 구조를 구상하는 것보다 눈앞의 더티한 코드를 직접 보면서 수정해 나가는 것이 기능을 훨씬 빠르게 완성할 수 있다는 점이었다.
3. Blue - 리팩터링
Blue 단계에서는 테스트가 통과된 상태를 유지하면서 코드의 구조를 개선한다.
중복을 제거하고, 네이밍을 다듬고, 책임을 분리한다.
이 단계에서 테스트가 깨지면 잘못된 리팩터링이므로 즉시 롤백하고 다시 시도하면 된다.
예를 들어보자.
아래와 같이 constants에서 상수를 가져와 매직 넘버를 제거하는 리팩터링이 있을 수 있다.
export const LOTTO_RULES = Object.freeze({
NUMBER_COUNT: 6,
MAX_NUMBER: 45,
MIN_NUMBER: 1,
});
리팩터링(Blue) 단계에서 또 다른 고민이 생겼다.
로또 번호 관리를 위해 WinningLotto를 Lotto의 상속 구조로 분리하는 과정에서,
이것이 리팩터링인지 새로운 기능 구현인지 경계가 불분명하게 느껴졌다.
리팩터링의 정의를 다시 살펴보면, 외부 동작을 바꾸지 않으면서 내부 구조를 개선하는 것이다.
이 정의를 기준으로 판단하면 새로운 클래스 추가가 리팩터링인지 아닌지를 판별하는 기준은 명확해진다.
기존 테스트가 여전히 전부 통과하는가? (내부 구조를 개선한 후 기존 테스트가 여전히 전부 통과한다면 그것은 외부 동작을 바꾸지 않은 것이라고 할 수 있다.)
반면 Blue 단계에서 기존 테스트에 없는 새로운 기능까지 함께 구현했다면,
그것은 리팩터링이 아니라 새 기능 개발이므로 Red 단계부터 다시 시작했어야 한다.
새로운 클래스 추가 자체가 리팩터링이냐 아니냐보다는,
새로운 기능이 추가되느냐 아니냐를 기준으로 판단하는 것이 더 본질에 가깝다고 본다.
TDD 환경에서의 페어프로그래밍, 그리고 교체 주기
필자는 이번 미션을 통해 처음으로 페어프로그래밍 경험을 가져갈 수 있었다.
페어 프로그래밍은 두 명의 개발자가 하나의 컴퓨터 앞에 함께 앉아 하나의 코드를 공동으로 작성하는 개발 방식이다.
역할은 두 가지가 있다.
드라이버: 실제로 키보드를 잡고 코드를 작성하는 사람
네비게이터: 드라이버 옆에서 코드를 바라보며 더 큰 그림을 생각하는 사람
중요한 것은 "드라이버와 네비게이터가 동시에 사고하고 있어야 한다"는 점!
TDD와 페어 프로그래밍을 함께 적용할 때, 드라이버와 네비게이터의 역할 교체 주기는 생각보다 까다로운 문제였다.
처음에는 일반적으로 권장되는 15분 주기로 역할을 교체하며 시작했다.
하지만 실제로 해보니 15분 안에 한 가지 기능의 Red-Green-Blue 사이클을 완전히 마무리하기 어려운 경우가 많았다.
사이클이 중간에 잘릴 때 흐름이 뚝 끊기는 느낌이 강했고,
다음 사람이 중간 상태의 코드를 이어받아 컨텍스트를 파악하는 데 시간이 걸렸다.
그래서 한 단위 테스트에 대한 Red-Green-Blue 사이클을 모두 완료한 후에 교체하는 방식으로 변경했다.
흐름이 끊기지 않는다는 장점이 있었지만, 하나의 사이클이 길어지면 네비게이터가 수동적으로 변하는 문제가 발생했다.
드라이버가 집중해서 코드를 작성하는 동안 네비게이터가 집중력을 잃게 되는 상황이 생겼다.
결국 선택한 방식은 단위 테스트 안에서 Red, Green, Blue 각 단계가 넘어갈 때마다 역할을 교체하는 것이었다.
여러 교체주기를 바꿔가며 시도해본 결과,
페어 프로그래밍에서 교체 주기를 결정하는 더 본질적인 기준은 두 사람이 동시에 사고하고 있는가?이지 않을까라는 생각이 들었따.
정해진 시간이나 단계가 아니라, 한 사람이 너무 오래 드라이버를 잡고 있어서 네비게이터가 딴 생각을 하기 시작하는 순간이 바로 교체 타이밍!!!
결국 페어 프로그래밍은 두 사람의 상호 호흡이 중요한 협업 방식이고, 교체 주기를 어떻게 맞추느냐보다 두 사람이 항상 같은 문제를 함께 고민하고 있는지를 보는 것이 더 핵심적인 질문이지 않을까 싶다.
'우아한테크코스' 카테고리의 다른 글
| [JavaScript] var / let / const 호이스팅과 TDZ (0) | 2026.03.10 |
|---|---|
| [JavaScript] 스코프(Scope) 개념정리 (1) | 2026.03.10 |
| [우아한테크코스 8기] 레벨 1 Gemini Canvas 웹앱 만들기 회고 (0) | 2026.03.01 |
| [우아한테크코스 8기] 웹 프론트엔드 최종 합격 후기 (4) | 2026.01.30 |
| [우아한테크코스 8기] 프리코스 오픈미션 회고 💡 (2) | 2025.11.25 |