티스토리 뷰

인터넷을 하다가 https://www.jetbrains.com/help/idea/replace-conditional-logic-with-strategy-pattern.html#5bed2 에서 아래와 같은 코드를 발견했다.

public class IfElseDemo {

    public double calculateInsurance(double income) {
        if (income <= 10000) {
            return income*0.365;
        } else if (income <= 30000) {
            return (income-10000)*0.2+35600;
        } else if (income <= 60000) {
            return (income-30000)*0.1+76500;
        } else {
            return (income-60000)*0.02+105600;
        }

    }
}

해당 사이트에서는 if else가 좋지 않은 코드를 만들기 때문에 어떻게 깔끔하게 고칠 수 있는지 보여주려고 했다.
if else가 나쁜 것이라고 단정짓기는 어렵지만, 좀 더 OOP스럽게 고칠 수 있다는 점에는 동의한다. 저 사이트에서 제공하는 솔루션을 보았다.

class IfElseDemo {
    private InsuranceStrategy strategy;

    public double calculateInsurance(double income) {

        if (income <= 10000) {
            strategy = new InsuranceStrategyLow();
            return strategy.calculateInsuranceVeryHigh(income);
        } else if (income <= 30000) {
            strategy = new InsuranceStrategyMedium();
            return strategy.calculateInsuranceVeryHigh(income);
        } else if (income <= 60000) {
            strategy = new InsuranceStrategyHigh();
            return strategy.calculateInsuranceVeryHigh(income);
        } else {
            strategy = new InsuranceStrategyVeryHigh();
            return strategy.calculateInsuranceVeryHigh(income);
        }

    }
}

난 잠시 생각했다. "뭔가 이상해". if else를 없앤다고 했는데, 제일 핵심적인 if else는 그래도 남아있다. 오히려 괜히 클래스만 더 만드는 수고를 했다. "이건 아니다" 라는 생각을 하면서 더 나은 방법에 대해서 생각해 보았다.

흔히 if else나 switch문은 별도의 클래스로 만들 수가 있다. 이 때의 장점은 OOP의 상속의 장점을 이용할 수가 있기 때문에 타입에 안전한 코드를 만들 수 있다.

먼저 인터페이스를 만들자.

interface IInsuranceCalculation {
    boolean accept();
    double calculate();
}

그리고 각각의 조건을 클래스로 만든다.

class LowIncome implements IInsuranceCalculation {
    private double income;

    public LowIncome(double income) {
        this.income = income;
    }

    @Override
    public boolean accept() {
        return income <= 10000;
    }

    @Override
    public double calculate() {
        return income * 0.365;
    }
}

class MediumIncome implements IInsuranceCalculation {
    private double income;

    public MediumIncome(double income) {
        this.income = income;
    }

    @Override
    public boolean accept() {
        return income <= 30000;
    }

    @Override
    public double calculate() {
        return (income - 10000) * 0.2 + 35600;
    }
}

class HighIncome implements IInsuranceCalculation {
    private double income;

    public HighIncome(double income) {
        this.income = income;
    }

    @Override
    public boolean accept() {
        return income <= 60000;
    }

    @Override
    public double calculate() {
        return (income - 30000) * 0.1 + 76500;
    }
}

class VeryHighIncome implements IInsuranceCalculation {

    private double income;

    public VeryHighIncome(double income) {
        this.income = income;
    }

    @Override
    public boolean accept() {
        return income > 60000;
    }

    @Override
    public double calculate() {
        return (income - 60000) * 0.02 + 105600;
    }
}

물론 위의 고정된 숫자들은 상수로 만들면 더 좋을 것이고 IInsuranceCalculationInterface대신 Abstract class를 쓸 수 있을 것이다.

그리고 클라이언트가 코드가 사용할 수 있는 중계 클래스가 필요하다. 실제 보험료 계산 클래스가 어떻게 선택되는지 클라이언트 쪽이 알게 할 필요가 없다.(Encapsulation)

class InsuranceCalculator {

    private List<IInsuranceCalculation> incomes = new ArrayList<>();

    public InsuranceCalculator(List<IInsuranceCalculation> incomes) {
        this.incomes = incomes;
    }

    public double calculate(double income) {
        for (IInsuranceCalculation calculation : incomes) {
            if (calculation.accept()) {
                return calculation.calculate();
            }
        }

        throw new UnsupportedOperationException("Cannot find suitable calculation");
    }
}

InsuranceCalculator에서 인컴 클래스들을 직접 생성할 수도 있겠지만, 외부에서 injection하는 방법도 있겠다. OOP에서는 한가지 목적에만 충실할 수록 좋은 코드를 만들 수 있는 확률이 더 올라가다는 원칙이 있다.(Single Responsibility Principle). 나중에 SpecialIncome 처럼 인컴 클래스가 더 늘어나게 되면 InsuranceCalculator를 손댈 필요가 없다.

이제 IfElseDemo 클래스를 수정할 차례다.

public class IfElseDemo {

    private InsuranceCalculator calculator;

    public IfElseDemo(InsuranceCalculator calculator) {
        this.calculator = calculator;
    }

    public double calculateInsurance(double income) {
        return calculator.calculate(income);
    }


}

이게 인컴 조건에 대한 변경사항이 오면 해당 인컴 클래스만 찾아서 고치면 된다. 또한 클래스 이름으로 인컴상태가 구분이 되므로 쉽게 해당 클래스를 찾을 수 있다.

마지막으로 테스트를 돌려본다. 기존 기능이 여전히 잘 동작하는지 검증하는 건 아주 중요하니까.

public class IfElseDemoTest {

    @Test
    public void low() {
        assertEquals(1825, insuranceFor(5000), 0.01);
    }

    @Test
    public void medium() {
        assertEquals(38600, insuranceFor(25000), 0.01);
    }

    @Test
    public void high() {
        assertEquals(78500, insuranceFor(50000), 0.01);
    }

    @Test
    public void veryHigh() {
        assertEquals(106400, insuranceFor(100_000), 0.01);
    }

    private double insuranceFor(double income) {
        return new IfElseDemo(
            new InsuranceCalculator(
                List.of(
                    new LowIncome(income),
                    new MediumIncome(income),
                    new HighIncome(income),
                    new VeryHighIncome(income)
                    )
            )
        ).calculateInsurance(income);
    }
}

테스가 모두 잘 동작한다. 굿잡~

'Object Oriented Programming > Refactoring' 카테고리의 다른 글

Erase Calcluations  (0) 2020.10.26
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함