클래스 추출

베루스·2023년 8월 6일
2
post-custom-banner

마틴 파울러의 ⌜리팩터링⌟에는 클래스 추출(Extract Class)이라는 기법을 설명하고 있습니다. IntelliJ에서 제공하는 Extract Delegate라는 Refactor 기능을 사용해 해당 기법을 적용할 수 있습니다. 간단한 예제를 통해 IntelliJ와 함께하는 클래스 추출을 살펴보려 합니다~

예제 코드

public class LottoMachine {

    private final int bonus;
    private final int[] winningLotto;
    private final List<int[]> issuedLottos;

    public LottoMachine(int bonus, int[] winningLotto, List<int[]> issuedLottos) {
        this.bonus = bonus;
        this.winningLotto = winningLotto;
        this.issuedLottos = issuedLottos;
    }

    public long getTotalPrize() {
        return issuedLottos.stream()
                .mapToLong(this::classifyPrize)
                .sum();
    }

    private long classifyPrize(int[] issuedLotto) {
        long matchesCount = matchesCount(issuedLotto);
        boolean isContainBonus = isContainNumber(issuedLotto, bonus);

        if (matchesCount == 6) {
            return 2_000_000_000L;
        }

        if (matchesCount == 5 && isContainBonus) {
            return 30_000_000L;
        }

        if (matchesCount == 5) {
            return 150_000L;
        }

        return 0;
    }

    private long matchesCount(int[] issuedLotto) {
        return IntStream.of(winningLotto)
                .filter(value -> isContainNumber(issuedLotto, value))
                .count();
    }

    private boolean isContainNumber(int[] issuedLotto, int number) {
        return IntStream.of(issuedLotto)
                .anyMatch(value -> value == number);
    }
}
class LottoMachineTest {
    @Test
    void getTotalPrize() {
        int[] winningLotto = {1, 2, 3, 4, 5, 6};
        int bonus = 7;
        List<int[]> issuedLottos = List.of(
                new int[]{1, 2, 3, 4, 5, 6}, // first prize = 2_000_000_000
                new int[]{1, 2, 3, 4, 5, 7}, // second prize = 30_000_000
                new int[]{1, 2, 3, 4, 5, 8} // third prize = 150_000
        );
        LottoMachine sut = new LottoMachine(bonus, winningLotto, issuedLottos);

        long actual = sut.getTotalPrize();

        assertThat(actual).isEqualTo(2_000_000_000L + 30_000_000L + 150_000L);
    }
}

위의 예제에서 LottoMachine은 발급한 로또의 등수와 총 상금액을 계산하고 있습니다. 현재는 복잡하지 않으므로, 클래스 추출이 필요하지는 않지만... 시간이 흘러 많은 책임이 LottoMachine에게 주어지고 있고, 몇 가지 책임의 분리가 필요하다고 가정하겠습니다. 그래서 발급한 로또의 등수를 판별하는 책임을 분리시키겠습니다.

Extract Delegate

클래스 추출을 원하는 메서드에 커서를 옮기고 Refactor This(⌃ + T)를 누르면 위의 캡처 이미지처럼 나오게 됩니다. 여기서 Extract Delegate를 선택해 클래스를 추출하고 위임으로 전환할 수 있습니다.

Extract Delegate 팝업 창이 나오면 추출할 새로운 클래스의 이름과 함께 이동할 멤버(메서드, 필드)를 선택할 수 있습니다. 추가적으로 추출될 메서드의 접근제어자도 지정할 수 있습니다. 추출 대상 메서드에서 다른 메서드에 대한 의존성이 있는 경우 이미지처럼 멤버 리스트 옆에 느낌표가 표시됩니다. 추출할 새로운 클래스의 이름은 우선 PrizeClassifer로 짓겠습니다.

이렇게 의존성을 가진 멤버를 모두 선택하게 되면 경고 표시를 제거할 수 있습니다. 이제 Refactor 버튼을 클릭하게 되면 아래와 같은 경고 창이 나오게 됩니다.

LottoMachine의 생성자에서 winningLottobonus를 초기화해주고 있고, 추출할 PrizeClassifier의 필드에 접근할 수 없기 때문입니다. Cancel을 누르고 이전 창으로 돌아가 Getter/Setter를 추가하도록 하겠습니다.

멤버 리스트 하단에 Generate accessors를 체크하면 접근자들을 추가로 생성할 수 있습니다. 이제 클래스를 추출하겠습니다.

추출 결과로 LottoMachine에서는 PrizeClassifier를 생성 및 초기화하고 관련된 메서드를 위임으로 전환하게 됩니다. matchesCountisContainNumber 메서드는 더 이상 사용하지 않으므로 제거하도록 하겠습니다.

추출된 PrizeClassifier에는 final 키워드로 인해 컴파일 에러가 발생하고 있는데 우선은 final 키워드를 제거해 문제를 해결하겠습다. 정상적인 클래스 추출을 확인하기 위해 작성한 테스트를 실행시키겠습니다.

짠~! 테스트가 통과 됐습니다!
앞으로 간단한 작업을 마치고 나면 꼭 테스트를 실행시켜 주기적으로 회귀를 방지하는 시간을 가질 겁니다.

PrizeClassifer 정리

IntelliJ가 추출해준 PrizeClassifer는 많이 지저분합니다. 그래서 직접 코드를 정리하는 시간을 가지겠습니다. 우선 LottoMachine에서 PrizeClassifer를 초기화하는 과정을 정리하겠습니다. Setter를 사용하지말고 생성자를 사용해 초기화하도록 하겠습니다.

우선 필요한 생성자를 생성합니다. 그리고 LottoMachine에서 PrizeClassifier를 초기화하는 로직을 추가한 생성자로 대체하겠습니다.

테스트 코드를 실행 시켜 문제가 없는지 확인합니다. 테스트를 통과하니 PrizeClassifier에서 사용하지 않는 생성자, Getter/Setter를 정리하고 필드에 final 키워드를 사용하겠습니다.

PrizerClassifier의 정리가 모두 마쳤습니다. 이제는 LottoMachine에서 정리가 필요한 부분을 찾고 수정하겠습니다!

LottoMachine 정리

classifyPrize 메서드의 구현은 위임만 남아있습니다. 위임을 제거하는 편이 더욱 간결하고 명확한 코드가 될 것으로 생각되어 인라인 시키도록 하겠습니다. classifyPrize 메서드에 커서를 옮기고 Inline Method(⌘ + ⌥ + N)를 실행시키겠습니다.

사용처 모든 곳에 인라인을 적용하고 기존 메서드는 제거하도록 선택하겠습니다. 그리고 테스트를 실행시켜 회귀를 방지하겠습니다. 위의 람다식은 메서드 레퍼런스로 변경할 수 있다고 IntelliJ가 알려주고 있습니다. 람다식으로 변경하겠습니다.

마무리

IntelliJ의 Extract Delegate를 사용해 원하는 클래스를 추출할 수 있습니다. 클래스를 추출하는 동기는 다양할 것 입니다. 이 부분에 대해서는 다루기 굉장히 복잡하므로... 마틴 파울러의 ⌜리팩터링⌟을 참고하시면 좋을 것 같습니다. 오늘도 IntelliJ와 함께하는 슬기로운 리팩터링 생활을 하시기 바랍니다~ ^^

post-custom-banner

4개의 댓글

comment-user-thumbnail
2023년 8월 6일

당신이 혹시 한국의 마틴인가요, 베루스?

1개의 답글
comment-user-thumbnail
2023년 8월 6일

감사합니다...감사합니다...감사합니다...

1개의 답글