본문 바로가기
기술/TDD

[켄트 백의 TDD] 테스트 주도 개발(TDD)

by dbjh 2021. 5. 24.
반응형

TDD(Test Driven Development)는 간단히 말해 테스트코드를 작성하고 해당 테스트코드가 정상적으로 실행되고 정확한 결과가 나오도록 확인하며 로직을 작성하면서 개발하는 방법이다. TDD의 궁극적인 목표는 작동하는 깔끔한 코드(Clean Code)를 만드는 것이다. 깔끔하지 못한 코드는 기능개선 및 유지보수를 힘들게하고 코드 한 줄의 수정이 불러올 후폭풍을 두렵게한다. 자동화된 테스트로 개발을 이끌어 나가서 우리의 개발시간을 효율적으로 사용할 수 있게 하는것이 바로 TDD(테스트 주도 개발)이다.

출처 : 구글검색

빨강 : 실패하는 테스트 코드를 먼저 작성한다. Compile조차 되지 않을 수 있다.
초록 : 빨리 테스트가 통과되도록 한다. 무슨짓(코드복사 등)을 해도 좋다.
리팩토링 : 중복 코드 제거 클린코드 작성 등의 리팩토링을 수행한다.

 

1.  TDD = 용기

빨강/초록/리팩토링은 TDD의 핵심구조이다. 이런식으로 프로그래밍을 하면 코드의 결함률을 극적으로 낮출 수 있고, 클린코드 작성이 가능하다. "별도의 테스트코드를 작성하는 시간이 필요하기 때문에 리소스가 더 많이드는 것 아닌가? 왜 이런 식으로 해야하지?" 라는 생각이 들 수도 있다. 하지만 그것은 매우 일시적인 것이다. 정산, 주문 등의 상당히 복잡한 로직들이 다수 포함된 프로젝트가 있을때, 기능추가 및 리팩토링을 하기 위해 상당한 고민의 시간이 필요하다. 내가 코드를 수정함으로써 서비스에 지장을 주진 않을까? 하는 두려움과 그로 인한 후폭풍을 고민하는등의 시간이 소요된다. 하지만 TDD는 이런 고민을 줄이고 안전성을 보장하기 위해 하는것이다. TDD는 결국, 개발을 하기 위한 용기를 주는 것이다.

 

2. 목록 나열

우선 어떤 것을 테스트할지 목록을 나열해보고 그에 맞게 테스트를 진행해보도록 하자. 혹시 놓친 것이 있다면 생각나는 대로 목록에 추가하도록하자.

$5 + 10CHF = $10(환율이 2:1일 경우)
$5 X 2 = $10

위처럼 진행할 목록을 나열했다면 하나씩 해결해보기로 하고 우선, 두번째 항목인 곱셈 부터 처리해보도록하자. 필요한 객체나 메소드 부터 만드는 것이 아니라 테스트를 먼저 만들어야 한다. 테스트를 작성할때는 기능(메소드의 집합)의 완벽한 인터페이스를 상상 하는것이 좋다.

아래의 테스트 코드를 보도록하자.

public void testMultiplication() {
   Dollar five = new Dollar(5);
   five.times(2);
   assertEquals(10, five.amount);
}

위의 테스트 코드를  살펴보면 Dollar 객체가 있는지, amount 필드의 접근지정자가 무엇인지 등등 실패상황이 발생할 것이다. 위에서 보이는 문제점을 다시 목록에 추가하도록하자.(확실한 목록명을 적기 애매하면 애매한상태로 적으면 된다.)

$5 + 10CHF = $10(환율이 2:1일 경우)
$5 X 2 = $10
amount를 private으로 만들기
Dollar 객체에서 발생하는 부작용(side effect)?
Money 소수점의 반올림?

위의 테스트코드는 컴파일 조차되지 않느 코드이다. 위에 코드에서 발생하는 에러는 아래와 같다.

  • Dollar 클래스 없음
  • 생성자 없음
  • times(int) 메소드 없음
  • amount 필드 없음

위의 에러를 하나씩 제거해나가면서 코드를 완성해보도록하자.

// Dollar 클래스를 만들어 첫번째 에러 해결
publc Class Dollar {
}

 

publc Class Dollar {
   // 생성자를 만들어 두번째 에러를 해결
   public Dollar (int amount) {
   }
}

 

publc Class Dollar {
   public Dollar (int amount) {
   }
   
   // times 메소드를 만들어 세번째 에러를 해결
   void times(int multiplier) {
   }
}

 

publc Class Dollar {
   // amount 필드를 만들어 네번째 에러를 해결
   int amount;

   public Dollar (int amount) {
   }
   
   void times(int multiplier) {
   }
}

 

위처럼 코드를 수정하고 실행을하면 테스트는 당연히 빨간불이 뜰 것이다. 자그럼 이 코드에서 최소한의 코드를 수정하여 초록막대를 보도록해보자

publc Class Dollar {
   // amount 필드를 10으로 초기화
   int amount = 10;

   public Dollar (int amount) {
   }
   
   void times(int multiplier) {
   }
}

위처럼 코드를 수정하고 테스트해보면 초록막대가 나타날 것이다. 테스트를 하는 주기는 아래와 같다.

1. 작은 테스트를 하나 추가한다.
2. 모든 테스트를 실행해서 테스트가 실패하는  것을 확인한다.
3. 조금 수정한다.
4. 모든 테스트를 실행해서 테스트가 성공하는 것을 확인한다.
5. 중복을 제거하기 위해 리팩토링을 한다.

총 5가지의 주기에서 우리는 4번까지 수행한 상태다. 어쨌든 수정을 해서 초록불을 확인했기 때문이다. 자 그렇다면, 5번을 진행해야하는데 위의 코드에서 중복이 어디있는지 알고 수행을하지 ? 라는 생각을 하게 될 것이다. 일반적으로 코드를 비교하겠지만 위의 경우에는 테스트 데이터와 코드에 있는 데이터 사이에 중복이 존재한다.

public void testMultiplication() {
   Dollar five = new Dollar(5);
   five.times(2);
   assertEquals(10, five.amount);
}

publc Class Dollar {
   // 10 = 5*2이기 때문에 아래처럼 수정
   int amount = 5 * 2;
}

// 10이라는 숫자도 만들어진 숫자이기 때문에 위처럼 변경

 

Dollar 생성자의 인자로 넣어준 5와 times() 메소드에 인자로 넣어준 2 가 Dollar 클래스의 amount 필드의 초기화 값과 중복이 된다. 중복 제거를 위해 초기화 시킨 필드를 times 메소드 안으로 옮겨 보도록하자.

publc Class Dollar {
   int amount;
   
   void times(int multiplier) {
      amount = 5 * 2;
   }
}

위처럼 코드를 수정하였으니 테스트 주기 1-4번을 수행하도록하자. 역시 초록불을 확인할 수 있다. 이렇게 작은단계로 진행해야하나? 라는 의문점이 들 수 있지만 TDD의 핵심은 이렇게 작은단계를 밟는 능력을 갖추는 것이다. 실제는 이런식으로 작업을 하진 않지만 정말 작은단계로 작업하는 방법을 배우면, 알아서 적절한 크기의 단계로 작업을 할 수 있을 것이다.

코드를 보면서 중복 or 이상한 부분을 하나씩 제거해보도록하자.

publc Class Dollar {
   int amount;
   
   public Dollar(int amount) {
      // amount 파라미터를 amount 필드에 저장
      this.amount = amount;
   }
   
   void times(int multiplier) {
      // amount 필드 * 2를 amount 필드에 저장
      amount = amount * 2;
   }
}

 

publc Class Dollar {
   int amount;
   
   public Dollar(int amount) {
      this.amount = amount;
   }
   
   void times(int multiplier) {
      // 상수 2를 multiplier 파라미터로 대체
      amount = amount * multiplier;
   }
}

위처럼 코드를 수정하면서 테스트를 진행하고 초록불을 확인하면된다. 이런식으로 TDD를 진행하는 것이다.

$5 + 10CHF = $10(환율이 2:1일 경우)
$5 X 2 = $10
amount를 private으로 만들기
Dollar 객체에서 발생하는 부작용(side effect)?
Money 소수점의 반올림?

여기까지 2번째 항목인 곱셈 테스트를 완료하였고, 우리는 이번글에서 아래와 같은 작업을 진행했다.

  • 작업목록 나열
  • 기능(메소드의 집합)이 외부에서 어떻게 보일지 코드로 표현
  • 막무가내로 코드를 수정하여 테스트를 통과시킴
  • 돌아가는 코드에서 상수를 변수로 변경하여 점진적으로 일반화
  • 새로운 할일들을 한번에 처리하지 않고 목록에 추가하고 넘어감

 

일반적인 TDD의 주기를 자세히 정리해보자.

1. 기능이 코드에 어떤식으로 나타나길 원하는지 생각하고 테스트를 작성한다. 흐름을 작성하는 것이다. 원하는 인터페이스를 개발하며 올바른 답을 위해 필요한 모든 요소를 포함한다.

2. 일단 초록불이 보이도록 만든다. 깔끔하고 단순한 방법이 생각나면 그것을 입력한다. 만약 그 방법을 구현하는데 어느정도의 시간이 필요하면 일단 적어만 놓은뒤 초록막대부터 나오도록 만든다. 

3. 초록막대를 보기위해 막무가내로 작성했던 코드를 일반화하고 중복을 제거한 훙 초록불이 들어오게하자.

최종적으로 우리는 "작동하는 깔끔한 코드"를 얻어야 한다. 깔끔한 코드를 얻는 것을 힘든 일이기 때문에 나누어서 처리하도록 하자.  일단 "깔끔한 코드"는 빼고 "작동하는"에 해당하는 부분을 먼저 처리하도록하자. 그러고 나서 "깔끔한 코드" 부분을 해결하는 것이다.

$5 + 10CHF = $10(환율이 2:1일 경우)
$5 X 2 = $10
amount를 private으로 만들기
Dollar 객체에서 발생하는 부작용(side effect)?
Money 소수점의 반올림?

테스트는 정상적으로 통과했지만 이상한 부분이 있다. Dollar의 연산을 수행 한 후 해당 Dollar의 값이 바뀌는 것이다.

public void testMultiplication() {
   Dollar five = new Dollar(5);
   five.times(2);
   assertEquals(10, five.amount);
   five.times(3);
   assertEquals(15, five.amount);
}

위의 테스트를 실행하면, 처음에 2와 5를 곱한 금액이 amount 필드에 저장이되고 바로 3을 곱하기 때문에 amount 필드의 값은 30이 될것이다. 

위의 문제를 해결하기 위해서 일단 times에서 새로운 Dollar 객체를 반환하게 해보자. 그리고 테스트 코드도 그에 맞게 수정하도록 하자.

public void testMultiplication() {
   Dollar five = new Dollar(5);
   Dollar product = five.times(2);
   assertEquals(10, five.amount);
   product = five.times(3);
   assertEquals(15, five.amount);
}

위와 같이 테스트 코드를 수정했다면, 당연히 에러가 발생할 것이다. times() 메소드의 반환 타입이 void 이기 때문에 이부분도 수정해주도록하자. 그러면 우리는 4번째 항목을 처리한 것이다.

publc Class Dollar {
   int amount;
   
   Dollar times(int multiplier) {
      return new Dollar(amount * multiplier);
   }
}
$5 + 10CHF = $10(환율이 2:1일 경우)
$5 X 2 = $10
amount를 private으로 만들기
Dollar 객체에서 발생하는 부작용(side effect)?
Money 소수점의 반올림?

 

최대한 빨리 초록색 불을 보기위한 방법 3개 중 2 개는 아래와 같다.

  • 가짜로 구현하기 : 상수를 반환하게 만들고 진짜 코드를 얻을때 까지 단계별로 상수를 변수로 바꾸어 간다.
  • 명백한 구현 사용하기 : 실제 구현을 입력한다.

위와 같이 진행하면서 초록색 불을 확인하고 내가 무엇을 입력해야 할지 확실히 알때는 명백한 구현을 더해 나간다. 물론 구현을 하면서 테스트를 주기적으로 실행해주도록 한다. 만약 빨간 불이 보이면 가짜로 구현하고 올바른 코드로 리팩토링 하도록 하자.  그렇게 다시 자신감이 생기면 명백한 구현하기를 진행하자.

3개 중 나머지 1개는 삼각측량(triangulation)이라 부르는 방법인데, 다음 글에서 보도록하자.

 

반응형

댓글