관리 메뉴

너와 나의 스토리

TDD(Test-Driven Development) 연습해보기 - 예제 1: Money (1) 1~8장 본문

개발/TDD(Test-Driven Development)

TDD(Test-Driven Development) 연습해보기 - 예제 1: Money (1) 1~8장

노는게제일좋아! 2021. 2. 25. 10:44
반응형

"테스트 주도 개발 Test-Driven Development: By Example" 책에 나오는 예제를 실제로 구현해보자.

 

 

프로그래밍 순서

  1. 빨강 - 실패하는 작은 테스트를 작성한다. 처음에는 컴파일조차 되지 않을 수 있다.
  2. 초록 - 빨리 테스트가 통과하게끔 만든다. 이를 위해 어떤 죄악을 저질러도 좋다.
    • 죄악: 테스트 통과만을 위해 복붙 하거나 함수에서 임의의 값을 무조건 리턴하게 구현하는 것.
  3. 리팩토링 - 2단계에서 생겨난 모든 중복을 제거한다.

 

 

예제 1: Money 

  • 다중 통화를 지원하는 보고서를 만들어보자.
종목 가격 합계
IBM 1000 25USD 25000USD
Novartis 400 150CHF 60000CHF
    합계 65000USD
기준 변환 환율
CHF USD 1.5
  • 이러한 보고서를 생성하려면 어떤 기능들이 필요할까?
    1. 통화가 다른 두 금액을 더해서 주어진 환율에 맞게 변한 금액을 결과로 얻을 수 있어야 한다.
    2. 어떤 금액(주가)을 어떤 수(주식의 수)에 곱한 금액을 결과로 얻을 수 있어야 한다. 
1. $5 + 10CHF = $10 (환율이 2:1일 경우)
2. $5 x 2 = $10
  • 위 요구사항을 구현하기 앞서 먼저 테스트를 만들자. 
    • 1번보다 2번 기능이 간단해 보이므로 2번부터 테스트 코드를 작성해보자.

 

 

Test 1: 어떤 금액(주가)을 어떤 수(주식의 수)에 곱한 금액을 결과로 얻을 수 있어야 한다.

  • 간단한 곱셈을 수행하는 테스트 코드를 작성하자

  • 위 코드에서는 4개의 컴파일 에러가 발생한다.
    1. Dollar 클래스 없음
    2. 생성자 없음
    3. times(int) 메서드가 없음
    4. amount 필드가 없음

 

1. Dollar 클래스 정의

 

2. 생성자 생성

 

3. times 메서드 생성

 

4. amount 필드 추가

 

  • 이제 컴파일이 가능하다. 그리고 다음과 같이 테스트가 실패하는 모습을 볼 수 있다.

 

  • 테스트가 성공하도록 수정해보자.

 

  • 원하는 기능을 하도록 수정해보자.

 

  • 하지만 아직 Dollar에 대한 문제점이 있다.
    • times 메서드를 수행하면 amount 값이 변경되는 것이다. 

 

  • 올바른 금액을 갖는 새로운  Dollar를 반환하도록 수정하자.

  • 위 테스트 코드를 다음과 같이 수정할 수 있다.

  • 위 테스트 코드를 돌리면 객체 자체는 다르기 때문에 다음과 같이 에러가 발생하게 된다.

  • 이를 위해, 동치성을 일반화해줄 equals 메서드를 작성해주면, 테스트가 성공하게 된다.

 

  • 수정된 테스트 코드에서는 더 이상 amount 변수를 직접 사용하는 일이 없어졌다. 
    • 즉, 변수를 private으로 변경할 수 있다.

 

 

 

 

Test 2: 통화가 다른 두 금액을 더해서 주어진 환율에 맞게 변한 금액을 결과로 얻을 수 있어야 한다.

 

  • 예: $5 + 10CHF = $10 (환율이 2:1일 경우)
  • Dollar 객체와 비슷하지만 달러 대신 Franc을 표현할 수 있는 객체가 만들어 단위가 섞인 덧셈 테스트를 작성하고 돌려보자.

 

 

 

 

 

리팩토링

<프로그래밍 순서>
1. 빨강 - 실패하는 작은 테스트를 작성한다. 처음에는 컴파일조차 되지 않을 수 있다.
2. 초록 - 빨리 테스트가 통과하게끔 만든다. 이를 위해 어떤 죄악을 저질러도 좋다.
3. 리팩토링 - 2단계에서 생겨난 모든 중복을 제거한다.
  • 이제 테스트는 통과하니 리팩토링을 해보자.
  • Dallar와 Franc 클래스의 공통 상위 클래스를 생성하자. -> Money 클래스
    • 그리고 Money 클래스가 공통의 equals 코드를 갖게 하자.

 

  • Money 클래스를 생성해주고, 하위 클래스에서 변수를 볼 수 있도록 amount 변수를 protected로 변경하자.

  • 이제 equals() 코드를 Money로 올려보자 -> 상위 클래스에서만 해당 메서드를 선언함으로써 중복을 줄이자.

  • Franc/Dollar.equals()와 Money.equals()는 거의 비슷해 보인다. 이 두 부분을 완전히 똑같이 만들 수 있다면 프로그램의 의미를 변화시키지 않고도 Franc/Dolloar의 equals()를 지워버릴 수 있다.
    • 이 경우에도 동치성 판단을 제대로 하는지 확인하기 위해 테스트 코드를 작성해보자.

  • 두 클래스의 equals() 지우고, 위 테스트 돌리면 성공한다. 하지만 적절한 테스트가 아니다. 왜냐하면, 서로 다른 통화를 비교하는 테스트가 존재하지 않기 때문이다. 
    • 두 클래스에서 amount 변수도 지워주자.
  • 즉, assertFalse(new Franc(5).equals(new Dollar(5))); 테스트 코드를 추가하면 테스트는 실패한다. 
    • 오직 금액과 클래스가 서로 동일할 때만 두 Money가 같다고 판단해야 한다.
  • 이처럼, 적절하지 못한 테스트 코드를 작성하면 리팩토링 과정에서 실수했는데도 불구하고 테스트가 통과해 오류를 탐지하지 못할 수도 있다.

  • equals() 메서드를 다음과 같이 수정하면 아래 테스트가 성공하는 것을 확인할 수 있다.

 

이번에는 공통 코드 부분인 times()를 해결할 차례이다.

  • 양쪽 모두 Money를 반환하게 만들면, 더 비슷하게 만들 수 있다.

 

 

객체 만들기

Dollar, Franc 두 클래스의 times() 구현 코드가 거의 똑같다. Money의 두 하위 클래스는 그다지 많은 일을 하는 것 같지 않으므로 아예 제거해버리고 싶다. 

  • 하위 클래스에 대한 직접적인 참조를 줄여보자 -> 하위 클래스를 제거하기 위해 한 발짝 다가서자.
  • 먼저, Money에 Dollar를 반환하는 팩토리 메서드(factory method)를 도입할 수 있다.
    • factory method: 부모 클래스에 알려지지 않은 구체 클래스를 생성하는 패턴이며, 자식 클래스가 어떤 객체를 생성할지를 결정하도록 하는 패턴이다.

  • Dollar에 대한 참조가 사라지게 하기 위해 테스트의 선언부를 다음과 같이 바꿔보자. 

  • 이제(와서?) Money를 추상 클래스로 변경한 후 times() 메서드를 선언하자.

  • 그럼, 위의 테스트를 돌리면 성공할 것이다.
  • 이제 팩토리 메서드를 테스트 코드의 나머지 모든 곳에서 사용할 수 있다.

  • 이제 클라이언트 코드에서 Dollar 클래스 존재를 알지 못한다. 하위 클래스의 존재를 테스트에서 분리함으로써 어떤 모델 코드에도 영향을 주지 않고 상속 구조를 마음대로 변경할 수 있게 됐다.
  • Franc도 Dollar처럼 수정해주자.

  • 정리:
    • 동일한 메서드 times()의 두 변이형 메서드 서명부를 통일시킴으로써 중복 제거를 향해 한 단계 더 전진했다.
    • 최소한 메서드 선언부만이라도 공통 상위 클래스로 옮겼다.
    • 팩토리 메서드를 도입하여 테스트 코드에서 콘크리트 하위 클래스의 존재 사실을 숨길 수 있었다.
      • 콘크리트(Concrete) 클래스: new 키워드를 사용하여 인스턴스를 만드는 클래스

 

 

 

 

 

출처:

- [테스트 주도 개발 Test-Driven Development: By Example"]

 

 

반응형
Comments