[수업] 로또 피드백 - 레거시 코드 TDD, cache
단위테스트와 TDD는 다르다.
단위테스트는 프로덕션 코드에 대한 테스트코드를 작성하는 것이라면,
TDD는 실패하는 컴파일 되는 테스트코드를 기반으로 프로덕션 코드를 작성해 나가는 것을 의미한다.
레거시 코드 TDD로 리팩토링하기
단위가 큰 프로젝트일수록 TDD를 진행하기가 어렵다. 특히, 리팩토링을 하거나 새로운 기능을 추가하게 되는 경우가 많은데 이럴 경우 프로덕션 코드 하나에 따른 다수의 테스트코드 수정이 발생할 수 있다.
✨점진적 리팩토링 - 오버로딩
: 컴파일이 가능하게 기존 메소드를 유지한 채, 시그니처가 다른 수정 메소드를 하나 더 만든다. (메소드 오버로딩)
생성자나 변수의 경우에도 마찬가지이다. 컴파일이 가능하게 중복을 유지한 채로, 점진적인 수정을 진행한다. 리팩토링을 하면서 중간에 배포될 수도 있고, 기존 상황으로 되돌아가야할 수도 있다. 내가 생각할 땐, 어느시점에서든 ✨컴파일 되는 상황을 유지하는 것이 중요하다고 생각한다. (돌아가는 시스템이 가장 우선된다.)
컴파일되는 상황을 유지한 채(중복되는 상태인 과도기적 상태를 유지), 점진적으로 리팩토링하고, 테스트코드를 유지해나가는 연습이 필요하다. 이 경우에도 TDD의 원칙은 유지한채 작성해나갈 수 있다.
Getter를 지양하라 - 객체값으로 비교
생성자가 setting의 역할을 하기 때문에 불변객체가 되기위해 setter는 지양된다. 더불어, getter도 가능한 지양하도록 한다. equals, hashcode로 객체비교를 진행해야 한다. 하지만, 원시값을 포장하였는데 view에서 그 내용 값을 보여줘야 한다면 getter를 사용할 수 밖에 없지 않을까 하는 생각도 든다.
Cache - 적절한 자료구조✨를 생각하자!
Getter를 사용하지 않는다면 배열로 캐싱을 하여 사용할 때, 해당 인덱스에 내가 원하는 객체가 있는지 어떻게 비교하지? 고민이 많이 되었다. 그렇다면 더 적합한 자료구조가 있는지 생각해보자!
public class LottoNumber {
public static final int MINIMUM_NUMBER = 1;
public static final int MAXIMUM_NUMBER = 45;
private static final LottoNumber[] cache = new LottoNumber[MAXIMUM_NUMBER];
private final int number;
static {
for (int i = 0; i < MAXIMUM_NUMBER; i++) {
cache[i] = new LottoNumber(i + MINIMUM_NUMBER);
}
}
private LottoNumber(int number) {
validateNumberRange(number);
this.number = number;
}
public static LottoNumber valueOf(int i) {
if (i <= MAXIMUM_NUMBER && i >= MINIMUM_NUMBER) {
return LottoNumber.cache[i - MINIMUM_NUMBER];
}
return new LottoNumber(i);
}
//...
}
Map은 캐싱을 할 때 많이 사용된다고 한다. Map<Integer, LottoNumber> cache 로 만든다면 인덱스 실수가 발생하지 않는다. 제대로 들어있는지 의심점이 적어진다. 뭔가 잘 안 풀린다면 적절한 자료구조를 사용하고 있는지 다시 생각해보자! 😀
public class LottoNumber {
public static final int MINIMUM_NUMBER = 1;
public static final int MAXIMUM_NUMBER = 45;
private static final Map<Integer, LottoNumber> cache = new HashMap<>(MAXIMUM_NUMBER);
private final int number;
static {
for (int i = MINIMUM_NUMBER; i <= MAXIMUM_NUMBER; i++) {
cache.put(i, new LottoNumber(i));
}
}
private LottoNumber(int number) {
validateNumberRange(number);
this.number = number;
}
public static LottoNumber valueOf(int i) {
if (i <= MAXIMUM_NUMBER && i >= MINIMUM_NUMBER) {
return cache.get(i);
}
return new LottoNumber(i);
}
//...
}
new 생성자로 호출되면 생성되는 것을 막을 수 없다.
위와 같이 valueOf를 사용하여 정적 팩토리 메소드를 구현하였다면 생성자는 닫아주는 것이 잘못된 생성을 방지할 수 있는 방안이 될 수 있다. 1~45 값 이외의 객체 생성을 막으려면 생성자를 막고, 범위 이외에서 exception을 발생시키면 된다.
public class LottoNumber {
//...
private static final LottoNumber[] cache = new LottoNumber[MAXIMUM_NUMBER];
private final int number;
static {
for (int i = 0; i < MAXIMUM_NUMBER; i++) {
cache[i] = new LottoNumber(i + MINIMUM_NUMBER);
}
}
private LottoNumber(int number) {
this.number = number;
}
public static LottoNumber valueOf(int i) {
if (i > MAXIMUM_NUMBER || i < MINIMUM_NUMBER) {
throw new CustomException("로또 넘버는 1~45 사이 정수이어야 합니다.");
}
return LottoNumber.cache[i - MINIMUM_NUMBER];
}
//...
}