Contents
테스트 주도 개발 1주차
   Oct 14, 2022     13 min read

스터디 1주차(22.10.08 ~ 22.10.15)

테스트 주도 개발 1주차 스터디

  • TDD 수련법부터 1부 4장 프라이버시까지

TDD 수련법


들어가는 글

상황

현재 회사는 달러로 명명된 채권만 다루고 있다.

런데 클라이언트가 새로운 채권 펀드를 시작하려고 하는데 다른 화폐로 채권을 다룰 필요가 생겼다고 한다.

따라서 다른 화폐로 채권을 다룰 수 있도록 로직을 확대해야한다.

그러나 쉽지않다.

요구사항

  • 다중 통화 구현

TDD 두 가지 단순한 규칙

  • 어떤 코드건 작성하기 전에 실패하는 자동화된 테스트를 작성하라.
  • 중복을 제거하라

1부 화폐 예제

1부에서는 테스트에 의해 주도되는 전형적 모델 코드를 개발하는 것이 목표이다.

테스트 주도 개발(TDD)의 리듬

  1. 재빨리 테스트를 하나 추가한다.
  2. 모든 테스트를 실행하고 새로 추가한 것이 실패하는지 확인한다.
  3. 코드를 조금 바꾼다.
  4. 모든 테스트를 실행하고 전부 성공하는지 확인한다.
  5. 리팩토링을 통해 중복을 제거한다.

글쓴이가 말하는 독자의 놀람 포인트

  • 각자의 테스트가 기능의 작은 증가분을 어떻게 커버하는지
  • 새 테스트를 돌아가게 하기 위해 얼마나 작고 못생긴 변화가 가능한지
  • 얼마나 자주 테스트를 실행하는지
  • 얼마나 수 없이 작은 단계를 통해 리팩토링이 되어가는지

1. 다중 통화를 지원하는 Money 객체

  • 다중 통화를 지원하는 Money 객체부터 시작.

  • 아래와 같은 보고서 있다고 가정

종목가격합계
IBM10002525000
GE40010040000
  합계65000
  • 다중 통화를 지원하는 보고서를 만들려면 통화 단위를 추가해야한다.
종목가격합계
IBM100025USD25000USD
Novartis400150CHF60000CHF
  합계65000USD
  • 환율
기준변환환율
CHFUSD1.5
$5 + 10CHF = $10(환율이 2:1일 경우)
$5 * 2 = $10

고민

  1. 새로운 보고서를 생성하려면 어떤 기능들이 있어야할까 ? (어떤 테스트들이 있어야할까?)
  2. 1번의 테스트들이 모두 통과했다면, 보고서에 제대로 계산되도록 하는 코드가 완성됐다는 걸 확신할 수 있을까?

할일 목록

  • 테스트 환경 구축

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

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

테스트는 무엇부터 하는게 좋을까? 작은 것 부터 차근차근 시작하는 것이 좋다.

첫 번째보다는 두 번째가 쉬워 보이기때문에 두 번째부터 진행하겠다.

➡️오퍼레이션(operation)이란 ? 보통 메소드와 비슷한 의미로 쓰이며 객체가 수행할 수 있는 연산을 의미한다. 엄격하게는 오퍼레이션에 대한 특정한 하나의 구현을 메서드라고 부른다.

  • 간단한 곱셈부터 냅다 해보자.

Untitled

$5 + 10CHF = $10(환율이 2:1일 경우)
$5 * 2 = $10
amount를 private으로 만들기
Dollar 부작용(side effect)?
Money 반올림?

위와같이 문제들을 기록하면서 진행한다.

게다가 결과는 컴파일 에러 투성이…

➡️에러 내용

  1. Dollar 클래스가 없다.
  2. 생성자가 없다
  3. times(int) 메서드가 없다
  4. amount 필드가 없다.

하나씩 컴파일 에러를 제거 해보자.

  • 우선 Dollar의 클래스를 만들어보자.
//Dollar
public class Dollar {
}
  • 두 번째 : 생성자를 만들자.
//Dollar
public class Dollar{

   Dollar(int amount){}
}
  • 세 번째 : times( )의 스텁 구현
    • 스텁 구현(stub implementation)이란?
      • 메서드의 서명부와(반환값이 있을 경우) 반환 명령만 적는 식으로 해서, 이 메소드를 호출하는 코드(이 경우엔 테스트 코드)가 컴파일 될 수 있도록 껍데기만 만들어 두는 것을 뜻한다.
//Dollar
public class Dollar {

    Dollar(int amount) {}

    void times(int multiplier){
    }

}
  • 마지막 : amount 필드 추가
//Dollar
public class Dollar {
    int amount;
    Dollar(int amount) {}

    void times(int multiplier){
    }

}

마지막까지 수정후 테스트 코드를 실행해 보았다.

Untitled 1

JUnit은 친절히 기대하는 값은 10이고 실제로 나온값은 0이라고 말해준다.

  • 성공하기 위해서 amount 값을 예측값인 10으로 변경해서 그린라이트를 만나보자.
int amount = 10;

Untitled 2

현재까지 한 것을 일반화하는 습관을 들이자. (주기)

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

➡️리팩토링 이란? 코드의 외적 행위는 그대로 유지하면서 내부 구조를 변경하는 작업을 뜻한다. 자세한 내용은 마틴파울러의 “Refactoring”을 참고

중복 제거

  1. 10은 값이 넘어온 값이다. 머릿속으로 계산한 후 넣은 값이기 때문에 중복을 제거해준다면 아래와 같다.
//Dollar
int amount = 10 ;  // --> int amount = 5 * 2;
  1. 5와 2를 한번에 제거 못한다. 그러나 객체의 초기화 단계에 있는 설정 코드를 times( ) 메서드 안에 옮길 수 있다.
//Dollar
int amount;

void times(int multiplier){
    amount = 5 * 2;
}
  1. amount에 5는 어디서 얻을 수 있을까? 생성자에서 넘어오는 값이다. 따라서 이걸 다음과 같이 amount변수에 저장하면,
//Dollar
Dollar(int amount){
    this.amount = amount;
}

그것을 times( )에서 사용할 수 있다.

//Dollar
void times(int multiplier){
    amount = amount * 2;

}

추가로 인자 time( ) 메서드에서 매개변수 multiplier 의 값이 2 이므로, 상수를 매개변수로 대체할 수 있다.

//Dollar
void times(int multiplier){
    amount = amount * multiplier;

}

마지막으로 코드의 간결성을 높이기 위해서 *= 연산자를 사용한다.

//Dollar
void times(int multiplier){
    amount *= multiplier;
}
  • 할일 목록 체크(수시로 )
$5 + 10CHF = $10(환율이 2:1일 경우)
~~$5 * 2 = $10~~
amount를 private으로 만들기
Dollar 부작용(side effect)?
Money 반올림?

현재까지 한 작업들 정리

  • 우리가 알 고 있는 작업해야 할 테스트 목록을 만듬
  • 오퍼레이션이 외부에서 어떻게 보이길 원하는지 말해주는 이야기를 코드로 표현함
  • JUnit에 대한 상세한 사항들은 잠시 무시하기로 함
  • 스텁 구현을 통해 테스트를 컴파일함
  • 끔직한 죄악을 범하여 테스트를 통과시킴
  • 돌아가는 코드에서 상수를 변수로 변경하여 점진적으로 일반화함
  • 새로운 할일들을 한번에 처리하는 대신 할일 목록에 추가하고 넘어감

2. 타락한 객체

일반적인 TDD 주기

  • 테스트 작성한다.
    • 마음 속에 있는 오퍼레이션이 코드에 어떤 식으로 나타나길 원하는지 생각해보기. → 이야기 쓰기?
    • 원하는 인터페이스를 개발하라. → 올바른 답을 얻기 위해 필요한 이야기의 모든 요소를 포함시키기
  • 실행 가능하게 만든다.
    • 빠르게 초록 막대를 보는 것. 중요하다. (심리적으로 안정)
    • 깔끔하고 단순한 해법이 명백히 보인다면 그것을 입력하라.
    • 만약 깔끔하고 단순한 해법이 있지만 구현하는 데 몇 분 정도 걸릴 것 같으면 일단 적어 놓은 뒤 원래 문제(초록 막대를 보는 것)로 돌아오기
  • 올바르게 만든다.
    • 시스템이 작동하면 직전에 저질렀던 죄악을 수습하자
    • 소프트웨어 정의(software righteousness)의 길로 되돌아와서 중복을 제거하고 초록 막대기로 되돌리자.
$5 + 10CHF = $10(환율이 2:1일 경우)
~~$5 * 2 = $10~~
amount를 private으로 만들기
**Dollar 부작용(side effect)?**
Money 반올림?

다시 들어가자.

Dollar 부작용

  • Dollar에 대한 연산을 수행한 후에 해당 Dollar의 값이 바뀐다.
    • times()를 처음 호출한 이후 five는 더 이상 5가 아니다.
@Test
    public void testMultiplication(){
        Dollar five = new Dollar(5);
        five.times(2);
        assertEquals(10, product.amount);
        five.times(3);
        assertEquals(15, product,amount);
}
  • 따라서 times( )에서 새로운 객체를 반환하게 만든다.

Untitled 3

컴파일에서 부터 막힌다. 컴파일 에러 수두룩하다.

Dollar.times() 수정하면 컴파일 에러는 막을 수 있다.

//Dollar
Dollar times(int multiplier){
    amount *= multiplier;
    return null;
}
  • 올바른 금액을 갖는 새 Dollar를 반환해야한다.
//Dollar
Dollar times(int multiplier){
    return new Dollar(amount * multiplier);
}

  • 할일 목록

  • $5 + 10CHF = $10(환율이 2:1일 경우)
  • $5 * 2 = $10
  • amount를 private으로 만들기
  • Dollar 부작용(side effect)?
  • Money 반올림?

초록 막대를 보기 위한 세 가지 전략

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

3. 모두를 위한 평등

  • 지금까지 해온 Dollar 객체 같이 객체를 값처럼 쓸 수 있는데 이것을 값 객체 패턴(value object pattern)이라고 한다.
  • 값 객체에 대한 제약사항 중 하나는 객체의 인스턴스 변수가 생성자를 통해서 일단 설정된 후에는 변하지 않는다는 것이다.

예를 들면)

수표가 하나 있는데 여기에 $5를 설정하고 또 다른 수표에도 전에 설정했던 $5를 설정했다고 가정해보자.

두 번째 값을 변화시키면 첫 번째 수표의 값까지 변하게되는 문제가 발생한다. 이것이 별칭 문제다.

하지만 값 객체를 사용한다면 별칭 문제에 대해서 걱정할 필요가 없어진다.

$5가 있을 때 값 객체 패턴으로 $5을 보장 받을 수 있다.

$6 or 7 … 등 원한다면 객체를 새로 만들어야한다.


  • 할일 목록 : equals() 추가

  • $5 + 10CHF = $10(환율이 2:1일 경우)
  • $5 * 2 = $10
  • amount를 private으로 만들기
  • Dollar 부작용(side effect)?
  • Money 반올림?
  • equlas()

값 객체를 암시하는 것

  1. 모든 연산은 새 객체를 반환해야 한다.
  2. 값 객체는 equals( )를 구현해야 한다는 것.
  3. 이유 : $5라는 것은 항상 다른 $5만큼이나 똑같이 좋은 것이기 때문이다. ???
  • 할일 목록 : hashCode() 추가

  • $5 + 10CHF = $10(환율이 2:1일 경우)
  • $5 * 2 = $10
  • amount를 private으로 만들기
  • Dollar 부작용(side effect)?
  • Money 반올림?
  • equals()
  • hashCode()

만약 Dollar를 해시 테이블의 키로 쓸 생각이라면 equals( )를 구현할 때에 hashCode( )를 같이 구현해야한다.

우선 equals()를 다뤄보자.

@Test
public void testEquality(){
    assertTrue(new Dollar(5).equals(new Dollar(5)));
}

➡️결과 : 빨간 막대

Untitled 4

빨간 막대 대처 : 가짜로 구현하기 → 단순히 true로 반환해주기

//Dollar
public boolean equals(Object object) {
    return true;
}

하지만 이렇게 가짜로 구현하는방법을 사용한다면 삼각측량 전략을 사용하지 못한다.

삼각 측량 전략

  • 만약 라디오 신호를 두 수신국이 감지하고 있을 때, 수신국 사이의 거리가 알려져 있고 각 수신국이 신호의 방향을 알고 있다면, 이 정보들만으로 충분히 신호의 거리와 방위를 알 수 있다. 이 계산법을 삼각측량이라고 한다.

  • 삼각측량을 이용하려면 예제가 두 개 이상 있어야만 코드를 일반화할 수 있다.

➡️삼각측량 - 예제) $5 ≠ $6을 해보자.

@Test
public void testEquality(){
    assertTrue(new Dollar(5).equals(new Dollar(5)));
    assertTrue(new Dollar(5).equals(new Dollar(6)));
}
  • 동치성(equality)을 일반화해야 한다.
//Dollar
public boolean equals(Object object) {
    Dollar dollar = (Dollar)object;
    return amount == dollar.amount;
}

동일성 문제 일시적으로 완료


  • 할일 목록 : eqauls() 완료

  • $5 + 10CHF = $10(환율이 2:1일 경우)
  • $5 * 2 = $10
  • amount를 private으로 만들기
  • Dollar 부작용(side effect)?
  • Money 반올림?
  • equals()
  • hashCode()

글쓴이는

  • 어떻게 리팩토링해야 하는지 전혀 감이 안 올 때만 삼각측량을 사용한다고 한다.
  • 코드와 테스트 사이의 중복을 제거하고 일반적인 해법을 구할 방법이 보이면 그냥 그 방법대로 구현한다고한다.

글쓴이는 설계를 어떻게 할지 떠오르지 않을 때면, 삼각측량은 문제를 조금 다른 방향에서 생각해본다고 한다.

➡️null 값 또는 다른 객체와 비교

우선 할일 목록에 추가만 해주자. 바로 필요하지 않기 때문에 써놓기만 한다.


  • 할일 목록 : Equal null, Equal object 추가

  • $5 + 10CHF = $10(환율이 2:1일 경우)
  • $5 * 2 = $10
  • amount를 private으로 만들기
  • Dollar 부작용(side effect)?
  • Money 반올림?
  • equals()
  • hashCode()
  • Equal null
  • Equal object

정리

  • 디자인 패턴(값 객체)이 하나의 또 다른 오퍼레이션을 암시한다는 것을 알았다.
  • 해당 오퍼레이션을 테스트했다.
  • 해당 오퍼레이션을 간단히 구현했다.
  • 곧장 리팩토링하는 대신 테스트를 조금 더 했다.
  • 두 경우를 모두 수용할 수 있도록 리팩토링했다.

4. 프라이버시


  • 할일 목록

  • $5 + 10CHF = $10(환율이 2:1일 경우)
  • $5 * 2 = $10
  • amount를 private으로 만들기
  • Dollar 부작용(side effect)?
  • Money 반올림?
  • equals()
  • hashCode()
  • Equal null
  • Equal object

개념적으로 Dollar.times( ) 연산은 호출을 받은 객체의 값에 인자로 받은 곱수만큼 곱한 값을 갖는 Dollar를 반환해야 한다. 하지만 테스트가 정확히 그것을 말하지는 않는다.

@Test
public void testMultiplication(){
    Dollar five = new Dollar(5);
    Dollar product = five.times(2);
    assertEquals(10, product.amount);
    product = five.times(3);
    assertEquals(15, product.amount);
}
  • 첫 번째 단언(assertion)을 Dollar와 Dollar를 비교하는 것으로 재작성할 수 있다.
    • assertEquals(new Dollar(10), product); 추가
@Test
public void testMultiplication(){
    Dollar five = new Dollar(5);
    Dollar product = five.times(2);
    assertEquals(new Dollar(10), product);
    product = five.times(3);
    assertEquals(15, product.amount);
}
  • 두 번째 단언 수정
    • assertEquals(new Dollar(15), product); 추가
@Test
public void testMultiplication(){
    Dollar five = new Dollar(5);
    Dollar product = five.times(2);
    assertEquals(new Dollar(10), product);
    product = five.times(3);
    assertEquals(new Dollar(15), product);
}
  • 임수 변수인 product는 쓸모 없음 → 인라인 시킨다.
public void testMultiplication(){
    Dollar five = new Dollar(5);
    assertEquals(new Dollar(10), five.times(2));
    assertEquals(15, five.times(3));
}

위와 같이 코드를 수정했을 때, Dollar의 amount 인스턴스 변수를 사용하는 코드는 Dollar 자신밖에 없게 됐다.

➡️따라서 변수를 pirvate으로 변경할 수 있다.

//Dollar
pirvate int amount;

할일 목록 : amount를 private으로 만들기 → 완료

  • $5 + 10CHF = $10(환율이 2:1일 경우)
  • $5 * 2 = $10
  • amount를 private으로 만들기
  • Dollar 부작용(side effect)?
  • Money 반올림?
  • equals()
  • hashCode()
  • Equal null
  • Equal object

하지만 여기서 주의해야할 상황이 생겼다.

만약 동치성 테스트가 동치성에 대한 코드가 정확히 작동한다는 것을 검증하는 데 실패한다면, 곱하기 테스트 역시 곱하기에 대한 코드가 정확하게 작동한다는 것을 검증하는 데 실패하게 된다.

이것은 TDD를 하면서 적극적으로 관리해야 할 위험 요소다.

지금까지 배운 것 정리

  • 오직 테스트를 향상시키기 위해서만 개발된 기능을 사용했다.
  • 두 테스트가 동시에 실패하면 망한다는 점을 인식했다.
  • 위험 요소가 있음에도 계속 진행했다.
  • 테스트와 코드 사이의 결합도를 낮추기 위해, 테스트하는 객체의 새 기능을 사용했다.

이상 들어가는 글 부터 4장 프라이버시까지…


글을 마치며..

TDD를 혼자하기 너무 두려움 마음이 컷다. 하지만 책으로 혼자하고 있었지만 많은 문제에 부딪혀 애를쓰고 있었다. 그러던중 마침 개발 오픈채팅방에서 TDD 스터디를 구한다는 공고를 보고 신청을했다. 처음 게더로 미팅을 했던 자리에서 자기 소개를 하는데 나빼고 다 실무자들이였다. 조금 위축이 되긴하지만 자신감을 갖고 자기소개를 하려고 노력했던거 같다. 한편으로는 잘하는 사람들과 스터디라….. 너무 기대도된다. 잘 모르지만 최선을 다해서 노력하겠다. 마지막으로 내 Github, 개인블로그에 있는 단어로 마지막을 장식하겠다.

I’m not a great programmer, I’m just a good programmer with great habits. -Kent Beck


참고 문헌 : 켄트 벡 지음, 김창준, 강규영 옮김 - Test-Driven Development