BackEnd/Refactoring

[Refactoring] 뒤엉킨 변경 (Divergent Change)

샤아이인 2023. 1. 4.

백기선 님의 리팩터링 강의를 들으며 요약한 내용입니다.

7. 뒤엉킨 변경

소프트웨어는 변경에 유연하게(soft) 대처할 수 있어야 한다.

 

어떤 한 모듈이 (함수 또는 클래스가) 여러가지 이유로 다양하게 변경되어야 하는 상황에서 이러한 모듈이 책임에 따라 잘 분리되어 있다면 변경에 대처하기가 쉽다.

 

예) 새로운 결제 방식을 도입하거나, DB를 변경할 때 동일한 클래스에 여러 메소드를 수정해야 하는 경우.

 

서로 다른 문제는 서로 다른 모듈에서 해결해야 한다.

 

모듈의 책임이 분리되어 있을수록 해당 문맥을 더 잘 이해할 수 있으며 다른 문제는 신경쓰지 않아도 된다.

 

7 - 1) 단계 쪼개기

다음과 같이 priceOrder라는 매우 다양한 역할을 하고 있는 메서드가 있다.

public class PriceOrder {

    public double priceOrder(Product product, int quantity, ShippingMethod shippingMethod) {
        final double basePrice = product.basePrice() * quantity;
        final double discount = Math.max(quantity - product.discountThreshold(), 0)
                * product.basePrice() * product.discountRate();
        final double shippingPerCase = (basePrice > shippingMethod.discountThreshold()) ?
                shippingMethod.discountedFee() : shippingMethod.feePerCase();
        final double shippingCost = quantity * shippingPerCase;
        final double price = basePrice - discount + shippingCost;
        return price;
    }
}

이를 좀더 의미 단위로 나누면 다음과 같다.

 

priceOrder가 무엇을 하는지 좀더 눈에 잘 들어온다.

public class PriceOrder {

    public double priceOrder(Product product, int quantity, ShippingMethod shippingMethod) {
        final PriceData priceData = calculatePriceData(product, quantity);
        return applyShinpping(priceData, shippingMethod);
    }

    private PriceData calculatePriceData(Product product, int quantity) {
        final double basePrice = product.basePrice() * quantity;
        final double discount = Math.max(quantity - product.discountThreshold(), 0)
                * product.basePrice() * product.discountRate();

        return new PriceData(basePrice, discount, quantity);
    }

    private double applyShinpping(PriceData priceData, ShippingMethod shippingMethod) {
        final double shippingPerCase = (priceData.basePrice() > shippingMethod.discountThreshold()) ?
                shippingMethod.discountedFee() : shippingMethod.feePerCase();
        final double shippingCost = priceData.quantity() * shippingPerCase;
        final double price = priceData.basePrice() - priceData.discount() + shippingCost;
        return price;
    }
}

또한 크게 단계를 2단계로 나누었다

1) 가격과 관련된 PriceData를 생성하는 단계

2) 가격에 실질적으로 할인을 적용하는 단계

 

이렇게 단계를 나누어 쪼개면 유지보수에 있어 편리해진다.

 

7 - 2) 함수 옮기기

▶ 함수를 옮겨야 하는 경우

해당 함수가 다른 문맥 (클래스)에 있는 데이터 (필드)를 더 많이 참조하는 경우.

해당 함수를 다른 클라이언트 (클래스)에서도 필요로 하는 경우.

 

함수를 옮겨갈 새로운 문맥 (클래스)이 필요한 경우에는 여러 함수를 클래스로 묶기 (Combine Functions info Class)” 또는 클래스 추출하기 (Extract Class)”를 사용한다.

 

다음과 같이 Account와 AccountType이 있다고 해보자!

Account

public class Account {

    private int daysOverdrawn;

    private AccountType type;

    public Account(int daysOverdrawn, AccountType type) {
        this.daysOverdrawn = daysOverdrawn;
        this.type = type;
    }

    public double getBankCharge() {
        double result = 4.5;
        if (this.daysOverdrawn() > 0) {
            result += this.overdraftCharge();
        }
        return result;
    }

    private int daysOverdrawn() {
        return this.daysOverdrawn;
    }

    private double overdraftCharge() {
        if (this.type.isPremium()) {
            final int baseCharge = 10;
            if (this.daysOverdrawn <= 7) {
                return baseCharge;
            } else {
                return baseCharge + (this.daysOverdrawn - 7) * 0.85;
            }
        } else {
            return this.daysOverdrawn * 1.75;
        }
    }
}

위 클래스에서 overdraftCharge가 Account에 있는것이 적합할까?

daysOverdrawn은 Account의 상태값이고, isPremium()은 AccountType의 것 이다.

따라서 overdraftCharge는 둘중 어디에 있어도 일단은 가능할것 같다.

 

▶ Account Type

public class AccountType {
    private boolean premium;

    public AccountType(boolean premium) {
        this.premium = premium;
    }

    public boolean isPremium() {
        return this.premium;
    }
}

 

overdraftCharge를 AccountType으로 이동시켜 보자!

public class AccountType {
    private boolean premium;

    public AccountType(boolean premium) {
        this.premium = premium;
    }

    public boolean isPremium() {
        return this.premium;
    }

    double overdraftCharge(int daysOverdrawn) {
        if (this.isPremium()) {
            final int baseCharge = 10;
            if (daysOverdrawn <= 7) {
                return baseCharge;
            } else {
                return baseCharge + (daysOverdrawn - 7) * 0.85;
            }
        } else {
            return daysOverdrawn * 1.75;
        }
    }
}

함수의 파라미터로 dayOverdrawn을 전달받게 된다.

책에서는 Account를 넘겨주고 있다 하셨는데, 이러면 순환 의존 문제가 발생해 버린다...

내 생각에서 변수 dayOverdrawn을 전달해주는것이  좋을 것 같다.

 

public class Account {

    private int daysOverdrawn;

    private AccountType type;

    public Account(int daysOverdrawn, AccountType type) {
        this.daysOverdrawn = daysOverdrawn;
        this.type = type;
    }

    public double getBankCharge() {
        double result = 4.5;
        if (this.daysOverdrawn() > 0) {
            result += this.type.overdraftCharge(this.daysOverdrawn()); // 변경
        }
        return result;
    }

    private int daysOverdrawn() {
        return this.daysOverdrawn;
    }
}

 

7 - 3) 클래스 추출하기

클래스가 다루는 책임(Responsibility)이 많아질수록 클래스가 점차 커진다. 책임을 분산시켜야 한다.

 

클래스를 쪼개는 기준?

 

1) 데이터나 메소드 중 일부가 매우 밀접한 관련이 있는 경우

2) 일부 데이터가 대부분 같이 바뀌는 경우 

3) 데이터 또는 메소드 중 일부를 삭제한다면 어떻게 될 것인가?

 

하위 클래스를 만들어 책임을 분산 시킬 수도 있고, 별도의 클래스로 분리할수도 있다.

댓글