우아한테크코스/레벨1

[수업] 로또 피드백 - 레거시 코드 TDD, cache

nauni 2021. 2. 26. 15:29

단위테스트와 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];
    }
//...
}