TDD : 테스트 주도 개발 방법론

2025. 3. 8. 22:41카테고리 없음

TDD 블로그 글 🚀

서론 🚀

Before TDD … 😅

내가 TDD를 배우기 전에 개발했던 방식은 다음과 같았다.

  1. 만들 기능에 대해서 설계를 고민한다. 🤔
    1. 어떤 클래스, 인터페이스가 필요할 지 고민하고 어떤 메소드가 들어갈지 오랜 시간 고민한다.
  2. 과정 1을 수행하면서 구현에 대해서도 고민한다.
  3. 바로 코드를 작성한다. 💻
    1. 리펙토링은 나중에 하기로 한다 (!!!!)
  4. 서버를 구동한 후, PostMan으로 원하는 결과가 나오는지 확인한다. 📡
    1. 오류나, 원하는 결과가 나오지 않는다면 원하는 결과가 나올 때까지 코드를 수정한다.

위 같은 과정으로 코드를 작성하다 보니, 오류가 발생하면 찾는데도 오래 걸렸으며 완성된 코드 조차 깔끔(클린)하지 못했다. 구현 시간이 촉박하다는 핑계로 테스트를 소홀히 하다 보니 다양한 케이스에 대한 예외처리도 많이 놓쳤던 것 같다. 무엇보다도 시간이 부족하여 테스트를 작성하지 않았다고 했지만, 테스트를 하지 않아도 개발 시간이 꽤 오래 걸렸다.

TDD를 배우기 전 했던 고민들 😵‍💫

  • 테스트 단위는 어느 정도로 ?
  • 테스트 할 때 다른 클래스에 대한 의존도가 너무 높은데 ?
  • 실제 DB를 사용하려니 부담 스러운데 ?

요즘 인기를 끌고 있는 개발 기법 TDD를 나 역시 이게 뭔지 들었던 기억이 있다.

‘테스트를 먼저 작성하고 구현’이라고 해서 독특하다고 생각했는데, 구현 코드가 없는데 어떻게 테스트를 할 수 있는거지? 라는 질문이 내 호기심을 자극했고, 이것이 내가 TDD를 배우게 된 계기가 되었다.

본론 🔥

TDD란?

TDD는 테스트부터 시작한다. 구현을 먼저 하고 나중에 테스트 하는 것이 아니라, 먼저 테스트를 하고 그 다음에 구현한다. 여기서 테스트를 먼저 한다는 것은 기능이 올바르게 동작하는지 검증하는 테스트 코드를 작성한다는 것을 의미한다. 기능을 검증하는 테스트 코드를 먼저 작성하고 테스트를 통과하기 위해 개발을 진행한다.

TDD 과정 : 덧셈 기능 검증을 위한 테스트 🧮

  1. TDD로 개발할 때 먼저 해야 할 것은 기능을 검증하는 테스트 코드를 작성하는 것이다.
    • 덧셈 기능을 검증하기 위해 테스트 코드를 아래와 같이 작성했다.
    
    public class CalculatorTest {
    
        @Test
        void plus() {
            int sum = Calculator.plus(1, 2);
            Assertions.assertEquals(3, sum);
        }
    }
            
    • @Test 어노테이션을 붙인 void plus()를 테스트 메소드로 인식한다.
    • 계산 기능을 실행하는 Calculator.plus(1, 2); 코드를 작성했다.
    • 계산 기능을 실행한 결과가 기대한 값인지 검증한다.
      • assertEquals는 인자로 받은 두 값이 동일한지 비교한다.
      • 이때, 첫 번째 인자는 기대한 값이고, 두 번째 인자는 실제 값이다.

    눈 여겨 볼 것은 아직 존재하지 않는 Calculator 클래스와 이 클래스의 plus 메소드이다.

    덧셈 기능을 제공할 클래스와 메소드는 어떤 이름을 가지면 좋을까? 고민해서 나온 네이밍이다.

    이 코드를 만들기 위해 아래와 같은 고민을 했다.

    • 메소드 이름은 뭐가 좋을까?
    • 메소드를 제공할 클래스 이름은 뭐가 좋을까?
    • 메소드는 파라미터가 몇 개여야 할까? 파라미터의 타입은? 반환은 어떻게?
    • 정적 메소드로 할까? 인스턴스 메소드로 할까?

    위와 같은 고민을 하고 코드를 작성했다. 그리고 assertEquals를 사용해서 실행결과가 올바른지 검증했다.

    1. 그러나 아직은 실행할 수 없다. Calculator 클래스를 생성하자

    
    public class Calculator {
        public static int plus(int a1, int a2) {
            return 0;
        }
    }
            

    - 메소드를 바로 구현하고 싶지만, 참고 저렇게 구현한 다음 테스트를 실행시켜 보자.

    - 실패했다. 나는 3으로 기대했으나, 실제 값은 0이어서 테스트에 실패했다.

    - 이 테스트를 통과시키는 가장 쉬운 방법은 무엇일까? 바로, plus() 메소드가 원하는 값을 반환하도록 상수를 반환하는 것이다.

    - 그냥 a1 + a2로 하면 될 것을… ← 하지만 이런 작은 단계를 차근차근 밟아가야 한다.

  2. 테스트를 통과시킬 만큼만 메소드를 구현한다.
    
    public class Calculator {
        public static int plus(int a1, int a2) {
            return 3;
        }
    }
            

    - 이번엔 이렇게 구현했고, 테스트 한 결과는 아래와 같다.

    - 테스트에 성공했다.

    - 테스트에 성공했으니, 덧셈 검증 코드를 하나 더 추가해보자

    
    @Test
    void plus() {
        int sum = Calculator.plus(1, 2);
        Assertions.assertEquals(3, sum);
        Assertions.assertEquals(5, Calculator.plus(4,1));
    }
            

    - 경우의 수를 하나 더 추가했다. 테스트를 통과하는지 확인해보자.

    - 새로 추가한 코드 때문에 테스트에 실패했다.

  3. 테스트를 통과시켜보자. 모두 통과시키도록.

    1+2와 4+1을 모두 통과시키도록 테스트 코드를 수정해야 한다.

    
    public static int plus(int a1, int a2) {
        if (a1 == 4 && a2 == 1) {
            return 5;
        }
        return 3;
    }
            

    - 이제 테스트를 통과하는지 보자.

    - 통과했다.

  4. 이렇게 점진적으로 구현을 완성해나간다.

    점진적으로 구현을 완성해 나가며 메서드 구현을 수정한다.

    
    public class Calculator {
        public static int plus(int a1, int a2) {
            return a1 + a2;
        }
    }
            

    - 다시 테스트를 수행해보면 역시 테스트를 통과한다.

  5. 테스트 폴더에 있던 코드를 src/main/java에 넣어 배포 대상에 포함시킨다.
  6. 코드를 이동하고 다시 테스트를 수행해서 테스트를 통과하는지 검사한다.

So … 😎

  • 앞서 TDD는 기능을 검증하는 테스트 코드를 먼저 만든다고 했다. 위 예시에서 덧셈 기능을 검증하는 테스트 코드를 먼저 작성했고, 이 과정에서 여러 고민을 했다. 이런 고민 과정은 실제로 설계 과정과 유사했다.
  • 테스트 코드를 작성한 뒤 필요한 클래스와 메소드를 작성하고 바로 테스트를 진행했는데, 실패하는 모습을 통해 문제점을 찾았다.
    • 실패한 이유를 확인하고 단순히 3을 리턴해서 테스트를 통과할 만큼만 코드를 구현했다.
  • 실패한 테스트를 통과시킨 뒤 새로운 테스트를 추가하고, 다시 그 테스트를 통과시키기 위한 코드만 작성했다.

이런 식으로 TDD는 테스트를 먼저 작성하고, 테스트에 실패하면 테스트를 통과시킬 만큼 코드를 추가하는 과정을 반복하면서 점진적으로 기능을 완성해 나간다.

결론 ✨

이렇게 TDD를 예제 코드를 통해 살펴봤다.

TDD에는 복잡한 문제도 가장 간단한 케이스(또는 의존 관계가 적은 것)부터 점진적으로 테스트를 추가하며 구현하는 것이 중요하다.

앞서, 필자는 plus() 메소드를 점진적으로 테스트하고 개선하면서, 바로 return a1 + a2;를 작성하지 않았다.

만약 초반부터 다양하고 복잡한 상황을 테스트로 추가하면 (해당 예제는 간단하지만) 해당 테스트를 통과시키기 위해 한 번에 구현해야 할 코드가 많아져, 결국 작은 단위로 테스트하지 못해 TDD의 흐름을 깨게 된다.

이렇게 빠른 실패를 맛보고, 자주 코드를 리팩토링 하며 성공도 실패 못지 않게 빠르게 확인함으로써 코드 작성에 자신감을 높일 수 있고, 나아가 테스트에 용이한 코드가 되어간다.

여태껏 왜 이렇게 개발을 안 했지? 싶을 정도로 신선한 충격을 준 개발 방법론이었다. 위 예제 외에도 다양한 예제를 경험하며 TDD의 효율성을 직접 체험했다 (단위 테스트, 통합 테스트, 모의 객체 활용까지).

앞으로의 개발에도 TDD를 적극적으로 활용해야겠다.