checklist
- $5 + 10CHF = $10 (환율이 2:1일 경우)
$5 * 2 = $10amount를 private 으로 만들기Dollar 부작용(side effect)?- Money 반올림?
equals()- hashCode()
- Equal null
- Equal object
- (new) 5CHF * 2 = 10CHF
목록에 있는 테스트 중에서 오랫동안 처리하지 못했던 첫번째 테스트를 처리해보도록 하자.
근데 저걸 한번에 처리하기엔 너무 큰 발걸음 같다. 작은단계 하나를 아래 추가해놓았다.
그럼 우리가 전에 만들었던 Dollar 객체와 비슷하게 생긴 Franc 객체를 만들어야 한다는 것을 알 수 있다. 그래서 그에 따른 테스트케이스도 동일하다. 고로 Dollar 테스트를 복사한 후 수정하자.
@Test
public void testFrancMultiplication(){
Dollar five= new Franc(5);
assertEquals(new Franc(10), five.times(2));
assertEquals(new Franc(15), five.times(3));
}
여기에서 초록 막대를 띄우려면 Dollar 코드를 복사해서 Dollar 을 Franc로 바꾸자.
public class Franc {
private int amount;
public Franc(int amount){
this.amount = amount;
}
Franc times (int multiplier){
return new Franc(amount * multiplier);
}
public boolean equals(Object object){
Franc franc = (Franc) object;
return amount == franc.amount;
}
}
근데 여기에서 엄청 불편할 것이다. 바로 코드 복붙으로 코드수가 2배로 늘어나고 중복이 일어났다는 점이다. 다시한번 우리가 TDD로 개발하는 프로세스를 살펴보자면
현재 1~4 까지는 빠르게 파악하여 복붙으로 진행했다. 앞서 말했듯이 1~4까지는 '어떤 죄악을 저지르더라도' 도달하기만 하면 되는 부분이다. 그리고 나서 리팩토링을 진행할 것이므로, 중복을 제거하는걸 다음 목표로 두고 넘어가도록 하자.
checklist
- $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
- 공용 times
우리는 위에서 5CHF * 2 = 10CHF 이 테스트를 통과시키기 위해서 Dollar 와 똑같이 작동하는 Franc 클래스를 만들고 그대로 복붙하여 일단 파란불이 들어오도록 하였다.
하지만 그에 비해 엄청난 중복이 일어났고, 코드의 양은 순식간에 불필요하게 2배로 불어나버렸다.
그래서 어떻게 고민하던 중 클래스의 상속을 통해 중복을 없애는게 적절하다 생각했고 상속은 아래 두가지 모델이 있을 것이다
둘다 해봐서 더 잘 줄어드는 걸 택해야겠지만, 딱봐도 오른쪽꺼가 적절해보인다.
사실 아주작은 단계로 나아가는 과정인만큼, 첫번째 모델도 해보고 넘어가는게 적절하다고 생각하지만, 책의 저자가 "해봤는데 별로"라고 넘어가버린만큼,,, 나중에 상속에 대해 근본적으로 다룰 기회가 있다면 다루도록 하자.
공통의 상위 클래스를 만들어 공통의 equals 코드를 갖게 하면 좋겠다고 생각했다.
일단 공통의 상위 클래스를 선언하고, Dollar 가 Money 를 상속받는다고 하자.
public class Dollar extends Money{
private int amount;
public Dollar(int amount){
this.amount = amount;
}
Dollar times (int multiplier){
return new Dollar(amount * multiplier);
}
public boolean equals(Object object){
Dollar dollar = (Dollar) object;
return amount == dollar.amount;
}
}
public class Money {
}
지금부터는 Dollar 에 있는 것들을 최대한 상위 클래스로 옮겨 중복을 줄여보도록 하자. (나중에 franc 도 상속받을 것을 목표로 하기 때문)
이제 amount
인스턴스 변수를 Money 로 옮길 수 있다.
public class Money {
protected int amount;
}
하위 클래스에서도 변수를 볼 수 있도록 가시성을 private -> protected 로 변경한다.
이제 마찬가지로 equals()
를 올릴 수 있게 되었다. equals()
상위 클래스로 올리기 전에 임시변수를 선언하는 부분을 변경하자.
public boolean equals(Object object){
Money money = (Money) object;
return amount == money.amount;
}
이제는 Franc 클래스의 equals()
를 수정해야 할 차례이다. 이 코드를 변경하기 전에 그에 따른 테스트 코드를 먼저 작성하고 넘어가도록 하자.
적절한 테스트를 갖지 못한 코드에서 TDD를 해야 하는 경우가 종종 있 을 것이다(적어도 향후 십년 정도는). 충분한 테스트가 없다면 지원 테스 트가 갖춰지지 않은 리팩토링을 만나게 될 수밖에 없다. 리팩토링하면서 실수했는데도 불구하고 테스트가 여전히 통과할 수도 있는 것이다. 어떻 게 할 텐가?
있으면 좋을 것 같은 테스트를 작성하라. 그렇게 하지 않으면 결국에는 리팩토링하다가 뭔가 깨트릴 것이다. 그러면 여러분은 리팩토링에 대해 안 좋은 느낌을 갖게 되고, 리팩토링을 덜 하게 된다. 리팩토링을 더 적게 하면 설계의 질이 저하되고, 결국 여러분은 해고될 것이다. 여러분의 강아 지도 곁을 떠날 것이고, 여러분은 자신의 영양 상태에 신경을 쓰지 못하게 될 것이다. 그러면 이도 나빠진다. 자, 이를 건강하게 유지하기 위해 리팩토링하기 전에 테스팅을 하자.
Dollar 에서 했던 것처럼 비슷한 테스트를 해야 하므로 그냥 복사하자.
@Test
public 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)));
}
근데 또 중복이 발생한 것을 확인할 수 있다. 나중에 고통받도록 하자.
Money 를 상속받기로 하고 amount 필드를 제거하자.
public class Franc extends Money{
public Franc(int amount){
this.amount = amount;
}
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.euqlas()
를 똑같이 만들 수 있다면 프로그램의 의미를 변화시키지 않고도 Franc 의 equals()
를 제거할 수 있게 된다.
public class Franc extends Money{
public Franc(int amount){
this.amount = amount;
}
Franc times (int multiplier){
return new Franc(amount * multiplier);
}
public boolean equals(Object object){
Money money = (Money) object;
return amount == money.amount;
}
}
public class Franc extends Money{
public Franc(int amount){
this.amount = amount;
}
Franc times (int multiplier){
return new Franc(amount * multiplier);
}
}
우리는 여태
equals()
구현을 일치시켰다.checklist
- $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- 공용 times
- Franc 와 Dollar 비교하기
불현듯 equals()
함수를 상위클래스 Money 로 이동시켜 리팩토링을 진행했는데 두 구현체 Dollar 와 Franc 를 비교하면 amount
만 비교하기에 같다고 나오지 않을까? 라는 걱정이 된다.
한번 테스트를 수정하여 실행시켜보자.
@Test
public 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)));
assertFalse(new Franc(5).equals(new Dollar(5)));
}
역시 빨간불이다.
equals()
함수에서 서로 다른 통화는 비교되었을 때 같다고 판단하지 않도록 한다.
public boolean equals(Object object){
Money money = (Money) object;
return amount == money.amount
&&getClass().equals(money.getClass());
}
모델 코드에서 클래스를 이런 식으로 사용하는 것은 좀 지저분해 보인다. 자바 객체의 용어를 사용하는 것 보다 재정 분야에 맞는 용어를 사용하고 싶지만, 현재는 통화(currency) 의 개념 같은 게 없고, 통화 개념을 도입할 충분한 이유가 없어 보이므로 써놓기만 하자.
checklist
- $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- 공용 times
Franc 와 Dollar 비교하기- 통화?