각종 후기/우아한테크코스

[우아한 테크코스 3기] LEVEL 2 회고 (112일차)

제이온 (Jayon) 2021. 5. 24.

안녕하세요? 제이온입니다.

 

오늘은 온라인으로 우테코를 진행했고, 협업 미션 리팩토링 외에는 크게 특별한 일은 하지 않았습니다.

 

 

협업 미션 리팩토링

저번 주에 조앤과 함께 협업 미션 3, 4단계를 끝내놨으나, 분기 처리를 이쁘게(?) 할 수 없을까 고민을 했었습니다. 우선, 기능 구현이 중요하다고 생각해서 이 부분은 뒤로 미뤄두었었죠. 그리고 그 미뤄둔 일을 오늘 오전 10시부터 진행했습니다.

 

저는 거리에 따른 추가 요금 로직과 연령에 따른 할인 로직을 전략 패턴으로 구현하려고 했습니다. 하지만, 전략을 외부에서 주입을 해 주어야 하는데, 거리나 연령에 따라서 전략이 정해져야하므로 if문이 필연적이었습니다. 그리고 그 거리나 연령이 상수이므로 그들을 어떻게 처리할지도 고민이었죠. 그래서 조앤과 저는 그 대신에 상수와 관련 로직을 Enum으로 빼기로 결정했습니다.

 

먼저, 연령에 따른 할인 정책을 어떻게 리팩토링했는지 보겠습니다. 아래 코드는 변경 전 코드입니다.

 

 

    public static int discount(int age, int fare) {
        if (age >= 19) {
            return fare;
        }
        if (age >= 13) {
            return (int) ((fare - 350) * 0.8);
        }
        if (age >= 6) {
            return (int) ((fare - 350) * 0.5);
        }
        return 0;
    }

 

 

6세 미만이면 무료, 6세 이상 13세 미만이면 50퍼 할인, 13세 이상 19세 미만이면 20퍼 할인, 그 위로는 할인이 없음을 알 수 있습니다. 이대로 코드를 두면, 각종 매직 넘버를 상수로 빼야하는데 해당 메소드가 정의된 FareCalculator의 필드로 상수가 잔뜩 생기게 됩니다. 그리고 분기 처리도 해당 클래스에서 진행하고 있죠.

 

 

    public static int discountFareByAge(int fare, int age) {
        final Discount discount = Discount.of(age);
        return discount.getDiscountedFare(fare);
    }

 

 

이를 다음과 같이 바꿨습니다. 상수와 자세한 할인 로직은 Discount에게 맡기고, FareCalculator 객체는 Discount 객체에게 할인된 금액을 반환해달라고 요청만 하면 됩니다.

 

 

public class Discount {
    private final DiscountRate discountRate;

    private Discount(DiscountRate discountRate) {
        this.discountRate = discountRate;
    }

    public static Discount of(int age) {
        return new Discount(DiscountRate.compareAgeBaseline(age));
    }

    public int getDiscountedFare(int fare) {
        return discountRate.calculateFare(fare);
    }
}

 

 

Discount 객체는 DiscountRate를 필드로 갖는데, DiscountRate는 Enum 형식으로 정의되어 있습니다.

 

 

public enum DiscountRate {
    TWENTIES(19, 0, 0),
    TEENAGERS(13, 350, 0.2),
    PRESCHOOLER(6, 350, 0.5),
    BABIES(0, 0, 0);

    private final int ageBaseline;
    private final int deductionFare;
    private final double discountRate;

    DiscountRate(int ageBaseline, int deductionFare, double discountRate) {
        this.ageBaseline = ageBaseline;
        this.deductionFare = deductionFare;
        this.discountRate = discountRate;
    }

    public static DiscountRate compareAgeBaseline(int age) {
        if (TWENTIES.ageBaseline <= age) {
            return TWENTIES;
        }
        if (TEENAGERS.ageBaseline <= age) {
            return TEENAGERS;
        }
        if (PRESCHOOLER.ageBaseline <= age) {
            return PRESCHOOLER;
        }
        return BABIES;
    }

    public int calculateFare(int currentFare) {
        return (int) ((currentFare - deductionFare) * (1 - discountRate));
    }
}

 

 

이렇게 연령에 기준선, 공제 금액, 할인률을 상수로 정의하고, 연령에 따른 분기 처리도 이곳에서 해 주었습니다. 또한, 이러한 정보를 바탕으로 할인된 금액까지 계산을 해 주고 있죠. 이를 통해 복잡한 연산과 상수 책임을 다른 객체에게 위임할 수 있습니다.

 

 

거리에 따른 추가 요금과 위 방식과 유사합니다. 다만, 이는 Enum 하나만으로도 적절해 보였습니다. 먼저, 변경 전 코드부터 보겠습니다.

 

 

public class FareCalculator {

    public static final int BASIC_DISTANCE = 10;
    public static final int MIDDLE_DISTANCE = 50;

    public static final int BASIC_FARE = 1250;
    public static final int OVER_FARE = 100;

    public static int calculateFare(int distance) {
        if (distance <= BASIC_DISTANCE) {
            return BASIC_FARE;
        }
        if (distance <= MIDDLE_DISTANCE) {
            return calculateExtraFareOfFirstRange(distance) + BASIC_FARE;
        }
        return calculateExtraFareOfSecondRange(distance) + BASIC_FARE;
    }

    private static int calculateExtraFareOfFirstRange(int distance) {
        return calculateOverFare(distance - BASIC_DISTANCE, 5, OVER_FARE);
    }

    private static int calculateExtraFareOfSecondRange(int distance) {
        int beforeFare = calculateExtraFareOfFirstRange(MIDDLE_DISTANCE);
        return beforeFare + calculateOverFare(
                distance - MIDDLE_DISTANCE,
                8,
                OVER_FARE
        );
    }

    private static int calculateOverFare(int distance, int overDistance, int overFare) {
        return (int) ((Math.ceil((distance - 1) / overDistance) + 1) * overFare);
    }

    public static int calculateFareWithLine(int basicFare, List<Line> lines) {
        return basicFare + lines.stream()
                .mapToInt(Line::getFare)
                .max()
                .orElseThrow(() -> new IllegalArgumentException("노선이 존재하지 않습니다."));
    }

    public static int discountFareByAge(int fare, int age) {
        final Discount discount = Discount.of(age);
        return discount.getDiscountedFare(fare);
    }
}

 

 

calculateFareWithLine() 전까지가 모두 거리에 따른 추가 요금을 합산한 요금 계산 로직입니다. 한 눈에 들어오지도 않고, 이것저것 상수도 필드로 정의되어 있습니다. 이러한 로직들을 ExtraFare로 옮겼습니다.

 

 

public enum ExtraFare {
    BASIC_FARE(0, 10, 0, 0, distance -> 0),
    FIRST_RANGE_FARE(10, 50, 5, 100, ExtraFare::getExtraFareOfFirstRange),
    SECOND_RANGE_FARE(50, Integer.MAX_VALUE, 8, 100, ExtraFare::getExtraFareOfSecondRange);

    private final int minRange;
    private final int maxRange;
    private final int overDistance;
    private final int overFare;
    private final UnaryOperator<Integer> calculator;

    ExtraFare(int minRange, int maxRange, int overDistance, int overFare,
        UnaryOperator<Integer> calculator) {
        this.minRange = minRange;
        this.maxRange = maxRange;
        this.overDistance = overDistance;
        this.overFare = overFare;
        this.calculator = calculator;
    }

    public static int calculateFare(int distance) {
        final ExtraFare extraFare = Arrays.stream(values())
            .filter(element -> element.minRange < distance && element.maxRange > distance)
            .findAny()
            .orElseThrow(() -> new IllegalArgumentException("해당하는 FareForDistance 객체가 없습니다."));

        return extraFare.calculator.apply(distance);
    }

    private static int getExtraFareOfFirstRange(int distance) {
        return calculateOverFare(distance - BASIC_FARE.maxRange,
            FIRST_RANGE_FARE.overDistance,
            FIRST_RANGE_FARE.overFare
        );
    }

    private static int getExtraFareOfSecondRange(int distance) {
        int beforeFare = getExtraFareOfFirstRange(FIRST_RANGE_FARE.maxRange);
        return beforeFare + calculateOverFare(
            distance - FIRST_RANGE_FARE.maxRange,
            SECOND_RANGE_FARE.overDistance,
            SECOND_RANGE_FARE.overFare
        );
    }

    private static int calculateOverFare(int distance, int overDistance, int overFare) {
        return (int) ((Math.ceil((distance - 1) / overDistance) + 1) * overFare);
    }
}

 

 

여기서 주목할 점은 필드로 함수적 인터페이스인 UnaryOperator를 정의했다는 점입니다. 그리고 아래에서 해당 연산 로직을 정의해서 생성자로 주입하는 방식입니다. 이를 통해 calculateFare() 메소드에서 다형성을 이용할 수 있게 됩니다. 참고로, UnaryOperator는 Function의 상속을 받고 있으며, Function<T, T>의 특수 형태입니다. 즉, input과 output의 자료형이 같다는 것이죠.

 

 

public class FareCalculator {

    private static final int BASIC_FARE = 1250;

    public static int calculateFare(int distance) {
        return ExtraFare.calculateFare(distance) + BASIC_FARE;
    }

    public static int calculateFareWithLine(int basicFare, List<Line> lines) {
        return basicFare + lines.stream()
            .mapToInt(Line::getFare)
            .max()
            .orElseThrow(() -> new IllegalArgumentException("노선이 존재하지 않습니다."));
    }

    public static int discountFareByAge(int fare, int age) {
        final Discount discount = Discount.of(age);
        return discount.getDiscountedFare(fare);
    }
}

 

 

결과적으로 FareCalculator 클래스의 코드가 간결해지고 가독성도 좋아졌습니다. 다만, 각각의 Enum 클래스 내에서 분기가 늘어난다면 코드가 늘어나는 것은 확실합니다. 특히, 거리에 따른 추가 요금은 각각의 전략을 메소드로 정의하고 있는데, 기준이 10개가 되면 메소드도 10개로 늘어난다는 문제가 있습니다. 하지만, 더이상 어떻게 리팩토링할 지 감이 안잡혀서 이 부분은 리뷰어님께 질문하기로 하였습니다.

 

 

정리

오후에는 김영한님의 스프링 기본편 강의를 들었으며, 협업 미션에서 자질구레한 부분을 리팩토링해 보았습니다. 그 외에는 오늘이 고시원 올라가는 날이라서 공부를 많이 하지는 못했습니다. 내일은 프론트엔드와 협업을 하는데, 순조롭게 api를 설계해서 PR을 날렸으면 좋겠습니다.

댓글

추천 글