타락한 객체

TDD 주기

일반적인 TDD 주기는 아래와 같이 진행한다.

테스트 작성한다.

오퍼레이션이 코드에 어떤 식으로 나타나길 원하는 지 생각해보고, 인터페이스 대한 이야기를 작성해보는 작업이다.

실행 가능하게 만든다.

이 단계에서 가장 중요한 것은 빠르게 초록 막대를 보는 것이다. 깔끔하고 단순한 해법이 명백히 보인다면 그것을 입력한다. 만약 깔끔하고 단순한 해법이지만 구현 시간이 몇 분 걸린다면 일단 적어 놓은 뒤 본래의 목적으로 돌아온다.

올바르게 만든다.

이 단계에서 이전에 시스템 동작(초록 막대)을 보기 위해 저질렀던 죄악을 수습한다. 중복을 제거하고 초록 막대로 되돌린다.

분할 정복

TDD 최종 목적은 "작동하는 깔끔한 코드"를 얻는 것이다. 작동하는 깔끔한 코드를 얻는 것은 때로는 최고의 프로그래머들 조차 도달하기 힘든 목표이며, 평범한 프로그래머들에게는 거의 불가능한 일이다. 따라서, "작동하는 깔끔한 코드"를 얻기 위해 문제를 분할하여 접근해보자. 결론부터 말하면, '작동하는 코드'를 만들고 나서 '깔끔한 코드' 로 변모시키는 것이다.

다시 다중화폐 예제에서 Dollar 부작용 테스트 돌아가 보자.

지금까지 테스트 하나를 통과했지만, 몇 가지 부자연스러운 결과를 알 수있다. Dollar 연산 수행시 Dollar 값 바뀐다는 것이다. 그러면 다시 TDD 주기의 "테스트를 작성한다." 단계에서 부터 접근을 해보자.

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

  • $5 * 2 = 10$

  • amount를 private으로 만들기

  • Dollar 부작용(side effect)?

  • Money 반올림?

우선 내가 원하는 시나리오를 적어보자. five Dollar 객체에 2를 곱하고, 3을 각각 곱하면 10, 15가 되는 자연스러운 시나리오이다.

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

}

두 번째로 실행 가능하게 만들어보자. 위 두번째 테스트에서 실패하게 된다.

왜일까? 이유는 4 라인을 수행하며 five Dollar 객체의 상태가 변하여 더이상 5가 아니기 때문이다.

그렇다면 times() 에서 새로운 객체를 반환하면 어떨까? 원래의 5달러를 가지고 온종일 곱하기를 수행해도 원래 5달러의 값은 변경되지 않을 것이다.

이를 적용해보기 위기 위해서는 Dollar 인터페이스와 테스트 모두를 변경해야만 하는데 괜찮은 선택인가??

어떤 구현이 올바른가에 대한 우리 추측이 완벽하지 못한 것과 마찬가지로 올바른 인터페이스에 대한 추측 역시 절대 완벽하지 못하다.

따라서, 첫 번째 단계로 돌아가서 위 생각을 반영하여 다시 테스트 코드를 작해보자.

@Test
void testMultiplication() {
    Dollar five = new Dollar(5);
    Dollar ten = five.times(2);
    assertEquals(10, ten.amount);
    Dollar fifteen = five.times(3);
    assertEquals(15, fifteen.amount);

}

역시나 컴파일 에러가 난다. 초록 막대를 보기 위해 에러를 빨리 제거해보자.

times() 반환 타입을 Dollar로 변경하고 Null 값을 반환하여 에러를 제거한다.

    public Dollar times(int multiplier) {
        amount *= multiplier;
        return null;

    }

에러를 제거한 덕분에 컴파일 되지만, 테스트 과정에서 null 값 참조하는 NullPointerException 예외가 발생하게 된다.

NullPointerException 예외를 제거하고 올바른 금액을 갖는 Dollar 를 반환하여 실행 가능하게 만들어야 한다.

public Dollar times(int multiplier) {
    return new Dollar(amount * multiplier);

}

결과적으로 초록 막대를 볼 수 있게 된다.

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

  • $5 * 2 = 10$

  • amount를 private으로 만들기

  • Dollar 부작용(side effect)?

  • Money 반올림?

초록 막대를 보기 위한 전략

이전 과정과 달리, 위 과정은 가짜 구현으로 시작해서 점차 실제 구현으로 바꿔나간 것이 아닌 올바른 구현이라고 생각한 내용으로 바로 실제 구현 과정을 적용하였다. 이와 같이, 최대한 빨리 초록 막대를 보기 위해 취할 수 있는 전략 중 두 가지는 아래와 같다.

  1. 가짜 구현 하기 : 상수를 반환하게 만들고 진짜 코드를 얻을 때까지 단계적으로 상수를 변수로 일반화해 나간다.

  2. 명백한 구현 사용하기 : 실제 구현을 입력한다.

여기서 켄트벡은 보통 실무에서 두 방법을 모두 사용한다고 설명한다. 명백하게 무엇을 구현해야 할 지를 알고 있다면 실제 구현을 입력하고 테스트를 하고, 만약 예상치 못한 빨간 막대를 만나게 된다면 가짜 구현 하기 방법을 사용하면서 올바른 코드로 리팩토링을 한다는 것이다.

마지막 한 가지 전략은 다음장에서 보게 될 삼각측량이라는 방법이다.

요약

이번 장에서 진행해본 것을 요약해 보면 아래와 같다.

  • 설계상의 결함(Dollar 부작용)을 그 결함으로 인해 실패하는 테스트로 변환하였다.

  • 스텁 구현으로 컴파일을 통과하도록 만들었다.

  • 올바르다고 생각하는 코드를 입력하여 테스트를 통과하였다.

느낌(부작용에 대한 혐오감)을 테스트(하나의 Dollar 객체에 곱하기를 두 번 수행하는 것)로 변환하는 것은 TDD의 일반적 주제이다. 이러한 작업을 오래 할수록 미적 판단을 테스트로 담아내는 것에 익숙해지며, 설계 논의에 깊이를 더할 수 있다.

Last updated