[TDD] 화폐 예제-2

수박참외메론·2022년 7월 25일
1
post-thumbnail

진행 과정

checklist

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

앞서 다중통화를 지원하는 포트폴리오 프로그램에 Dollar 객체를 TDD 방식으로 설계하는 과정해서 해낸 일을 정리해보자면.
1. 어떤 금액(주가)를 어떤 수(주식의 수)에 곱한 금액을 결과로 얻어내는 과정
2. Dollar 객체의 times() 가 예상한 값을 뱉어내지 않는 문제 해결

이 두가지를 해결해냈다.

3장. 모두를 위한 평등

들어가기 전에 : 값 객체

앞서 테스트에서 사용했던 Dollar 객체같이 five 객체를 값처럼 쓰는 패턴을 ‘값 객체 패턴’ 이라고 한다.
값 객체에 대한 제약사항 중 하나는 객체의 인스턴스 변수가 생성자를 통해서 일단 설정된 후에는 결코 변하지 않는다는 것이다. 그래서 이 값 객체를 사용하면 변수명을 쉽게 설정할 수 있다는 큰 장점이 있다.

값 객체를 사용하는 조건 중 하나는 모든 연산은 새 객체를 반환해야 된다는 것이다. 이는 2장에서 times()의 부작용을 해결할 때 새 객체를 반환하게 바꾼 적이 있었다.
값 객체의 사용할 때 지켜야 하는 또다른 조건은 값 객체는 equals() 를 구현해야 한다는 것이다. 이는 $5 가 $5 랑 같은 가치를 가지고 있는지 체크해야 하기 때문이다.

또 만약 Dollar 를 해시테이블의 key 로 쓸 생각이라면 equals()를 구현할 때 hashCode() 를 같이 구현해야 하는데 이는 다음에 문제가 있을 때 구현하는 걸로 하고 체크리스트에 추가만 해두자.

checklist

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

equals() 구현

테스트 구현

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

java 에서 기본적으로 모든 객체는 object 를 상속받기 때문에 equals 가 구현이 되어있어 컴파일 오류가 안 뜨는 것을 확인할 수 있다.
두 객체 자체를 비교하여 생성된 두 객체가 같은 객체인지를 확인하므로 false가 반환된다.

그래서 일단 빨간 막대는 보이는 상태이다.

빨간 막대 처리

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

우리는 여기서 5==5 라는 테스트를 amount == dollar.amount 로 비교하여 테스트를 통과하도록 만드는 방법이 있다는 사실을 알 것이다. 하지만 여기서는 테스트의 또다른 방식은 삼각측량 방식을 다뤄보자.

삼각측량

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

거창하게 삼각측량이라고 말했지만, 간단히 말하자면 하나의 테스트할 기능에 대해 두개 이상 종류의 상황을 테스트코드로 작성하면 기능의 generalization이 가능하다는 소리이다.

그러면 삼각측량을 위해 두번째 예제를 작성하자. $5 != $6 은 어떨까?

@Test
public void testEquality(){
    assertTrue(new Dollar(5).equals(new Dollar(5)));
    assertFalse(new Dollar(5).equals(new Dollar(6)));
}
public boolean equals(Object object){
    Dollar dollar = (Dollar) object;
    return amount == dollar.amount;
}

times() 를 일반화할 때도 삼각측량을 사용하면
1. $5 2 = $10
2. $5
3 = $15
이렇게 하여 더이상 상수를 return하는 것만으로 테스트를 통과할 방법이 없게 된다.

삼각측량도 우리가 단계를 하나하나 천천히 밟아나가는 것과 마찬가지로 설계를 어떻게 할 지 떠오르지 않을 때 사용하곤 한다. 삼각측량은 문제를 조금 다른 방향에서 생각해볼 기회를 제공한다.

동일성 문제는 일시적으로 해결되었으나, null 값이나 다른 객체들과 비교한다면 어떻게 될지는 머리속에 떠오르지만 나중에 해결하도록 하자.

checklist

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

정리

  • 우리의 디자인 패턴(값 객체)이 하나의 또다른 기능을 필요로 한다는 걸 알았다.
  • 해당 메서드을 테스트했다.
  • 해당 메서드을 간단히 구현했다.
  • 곧장 리팩토링하는 대신 테스트를 좀 더 했다.
  • 두 경우를 모두 수용할 수 있도록 리팩토링했다.

4장. 프라이버시

앞서 times() 연산은 호출 객체의 값에 인자로 받은 곱수 만큼 곱한 값을 갖는 Dollar를 반환해야 하지만 테스트는 정확히 말하면 그걸 말하고 있지는 않다.

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

위의 상황을 정확히 코드상 표현하려면 아래와 같아야 한다.

	@Test
    public void testMultiplication2(){
        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는 더이상 쓸모 없어 보이므로 중복을 제거(인라인시키는 것으로)하고

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

이렇게 테스트를 고치고 나니 Dollar의 amount 인스턴스 변수를 사용하는 코드는 Dollar 자신 밖에 없으므로 변수를 private으로 변경할 수 있다.

private int amount;

주의해야 할 사항

이렇게 할 때 테스트 코드에서 equals() 메서드가 정확히 작동하는데 검증하는데 실패한다면, times() 를 테스트하는 코드 역시 곱하기에 대한 코드가 정확하게 작동하는 것을 검증하는데 실패한다. 이런 테스트간의 관계는 TDD를 하면서 적극적으로 관리해야 할 위험 요소이다.

사실 우리의 코드는 결코 완벽할 수 없고 우리는 완벽함을 위해 노력하지는 않는다. 그저 모든 것을 두번 함으로써(테스트와 코드) 자신감을 가지고 전진할 수 잇을 만큼만 결함의 정도를 낮추기를 희망할 뿐이다. 그래서 때때로 우리의 추론이 맞지 않아서 결함이 손가락 사이로 빠져나갈때면 우리는 테스트를 어떻게 작성해야 했는지에 대한 교훈을 얻고 다시 앞으로 나아가야 한다.

정리

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

0개의 댓글