돌아온 '모두를 위한 평등'

앞 과정에서 하나의 테스트를 통과하기 위해 엄청난 중복 코드를 만들었기 때문에, 이제 중복 코드를 제거하는 리팩토링을 할 시간이다.

우선, Dollar와 Franc 두 클래스에서 Money 라는 공통 클래스를 구성하고 equals 코드를 갖게 하면 어떨까?

할일 목록을 업데이트 하고, 하나씩 진행해보자.

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

  • $5 * 2 = 10$

  • amount를 private으로 만들기

  • Dollar 부작용(side effect)?

  • Money 반올림?

  • equals()

  • hashCode()

  • Equal null

  • Equal object

  • 5CHF * 2 = 10CHF

  • Dollar/Franc 중복

  • 공용 equals

  • 공용 time

Money 공통 클래스

공통 Money 클래스를 생성하고 테스트를 진행해보자.

public class Money {
}

무리없이 테스트가 통과한다.

다음으로 Dollar가 Money를 상속하도록 변경하고 테스트해본다.

public class Dollar extends Money {
    private int amount;

    public Dollar(int amount) {
        this.amount = amount;

    }

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

    }

    public boolean equals(Object object) {
        Dollar dollar = (Dollar) object;
        return amount == dollar.amount;

    }

}

테스트 모두 잘 동작한다.

이제 amount 인스턴스 변수를 Money 로 옮기며 Dollar 클래스에서 참조 가능하게끔 protected 접근 범위로 변경한다.

public class Money {
    protected int amount;
    
}

Money Equals

이어서, equals() 메서드를 Money 클래스로 옮기는 작업을 천천히 진행해보자.

첫 번째로 임시변수 타입을 상위 타입으로 변경한다.

public boolean equals(Object object) {
    Money dollar = (Dollar) object;
    return amount == dollar.amount;

}

모든 테스트는 여전히 잘 돈다.

다음으로 캐스팅(cast) 타입도 변경해서 테스트 해보자.

public boolean equals(Object object) {
    Money dollar = (Money) object;
    return amount == dollar.amount;

}

테스트는 잘 동작하며, 조금 더 명확하게 변수명을 money로 변경해보자.

public boolean equals(Object object) {
    Money money = (Money) object;
    return amount == money.amount;

}

마지막으 equals 메서드를 Money 클래스로 옮겨보자.

public class Money {
    protected int amount;

    public boolean equals(Object object) {
        Money dollar = (Money) object;
        return amount == dollar.amount;

    }

}

적절한 테스트가 없는 코드에서의 TDD

Franc equals 도 제거해야 할 차례이다. 여기서 문제는 Franc 객체간의 동치성 테스트를 하지 않았다는 점이다.

적절한 테스트가 없는 경우, 어떻게 접근해야 할까?

충분한 테스트가 존재하지 않는다면 지원 테스트가 잘 갖춰지지 않은 리팩토링을 만날 수 밖에 없다. 결국, 리팩토링하면서 실수하였는데도 불구하고 여전히 테스트를 통과하는 위험을 초래할 수 있다.

어쩔 수 없다. 개발자 스스로가 리팩토링 전에 있으면 좋을 것이라고 생각하는 테스트를 작성하 그 범위를 좁혀 나가야만 한다.

Franc equals 경우에는 Dollar 동치성 테스트를 복사하면 된다.

@Test
void testEquality() {
    assertTrue(new Dollar(5).equals(new Dollar(5)));
    assertFalse(new Dollar(5).equals(new Dollar(6)));
    assertTrue(new Franc(5).equals(new Franc(5)));
    assertFalse(new Franc(5).equals(new Franc(6)));
    
}

동일한 중복코드로 만들어 제거하

이제 Dollar equals 와 동일하게 Franc equals 도 점진적으로 제거해 나간다.

Money 타입을 상속받고, amount 인스턴스 변수를 제거해보자.

public class Franc extends Money {
    public Franc(int amount) {
        this.amount = amount;

    }

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

    }

    public boolean equals(Object object) {
        Franc franc = (Franc) object;
        return amount == franc.amount;

    }

}

테스트는 여전히 잘 동작한다.

이제 Franc.equals() 와 Money.equals() 가 비슷해보이며, 완전히 동일하게 만들 수 있다면 프로그램의 의미를 변화시키지 않고 Franc.equals() 를 제거할 수 있을 것이다.

임시변수의 Franc 타입을 Money 로 변경해보자.

public boolean equals(Object object) {
    Money franc = (Franc) object;
    return amount == franc.amount;

}

다음으로 캐스팅도 상위 타입으로 변경한다.

public boolean equals(Object object) {
    Money franc = (Money) object;
    return amount == franc.amount;

}

결과적으로, Money.equals() 와 Franc.equals()는 완벽히 동일하기 때문에 Franc.equals()를 제거할 수 있다.

public boolean equals(Object object) {
    Money money = (Money) object;
    return amount == money.amount;

}

테스트 또한, 잘 동작하고 있다. 할일 목록에서 하나 더 선을 그을 수 있게 되었다.

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

  • $5 * 2 = 10$

  • amount를 private으로 만들기

  • Dollar 부작용(side effect)?

  • Money 반올림?

  • equals()

  • hashCode()

  • Equal null

  • Equal object

  • 5CHF * 2 = 10CHF

  • Dollar/Franc 중복

  • 공용 equals

  • 공용 time

  • Franc과 Dollar 비교하기

Last updated