리팩토링 - 냄새 7. 뒤엉킨 변경

김상운(개발둥이)·2022년 4월 4일
0

리팩토링

목록 보기
8/17
post-thumbnail

들어가기

해당 포스팅은 인프런 백기선님의 '리팩토링'을 학습 후 정리한 내용입니다.



냄새 7. 뒤엉킨 변경

Divergent Change

  • 소프트웨어는 변경에 유연하게(soft) 대처할 수 있어야 한다.
  • 어떤 한 모듈이 (함수 또는 클래스가) 여러가지 이유로 다양하게 변경되어야 하는 상황.
    • 예) 새로운 결제 방식을 도입하거나, DB를 변경할 때 동일한 클래스에 여러 메소드를 수정해야 하는 경우.
  • 서로 다른 문제는 서로 다른 모듈에서 해결해야 한다.
    • 모듈의 책임이 분리되어 있을수록 해당 문맥을 더 잘 이해할 수 있으며 다른 문제는 신경쓰지 않아도 된다.
  • 관련 리팩토링 기술
    • 단계 쪼개기 (Split Phase)”를 사용해 서로 다른 문맥의 코드를 분리할 수 있다.
    • 함수 옮기기 (Move Function)”를 사용해 적절한 모듈로 함수를 옮길 수 있다.
    • 여러가지 일이 하나의 함수에 모여 있다면 “함수 추출하기 (Extract Function)”를 사용할 수 있다.
    • 모듈이 클래스 단위라면 “클래스 추출하기 (Extract Class)”를 사용해 별도의 클래스로 분리할 수 있다.


단계 쪼개기

Split Phase

  • 서로 다른 일을 하는 코드를 각기 다른 모듈로 분리한다.
    • 그래야 어떤 것을 변경해야 할 때, 그것과 관련있는 것만 신경쓸 수 있다.
  • 여러 일을 하는 함수의 처리 과정을 각기 다른 단계로 구분할 수 있다.
    • 예) 전처리 -> 주요 작업 -> 후처리
    • 예) 컴파일러: 텍스트 읽어오기 -> 실행 가능한 형태로 변경
  • 서로 다른 데이터를 사용한다면 단계를 나누는데 있어 중요한 단서가 될 수 있다.
  • 중간 데이터(intermediate Data)를 만들어 단계를 구분하고 매개변수를 줄이는데 활용할 수 있다.

예제 코드

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;
    }
}

Product

public record Product(double basePrice, double discountThreshold, double discountRate) {

}

ShippingMethod

public record ShippingMethod(double discountThreshold, double discountedFee, double feePerCase) {
}

냄새

PriceOrder 클래스의 priceOrder() 함수에서 냄새가 난다!
해당 함수에서 주문에 값을 매기는 일을 하지만 내부에서 할인가격과 할인된 가격을 구하는 두가지 일을 같이한다.

해결

서로 다른 일을 하는 코드를 각기 다른 함수로 분리하자! 단계를 쪼개보자!

리팩토링 후

PriceOrder

public class PriceOrder {

    public double priceOrder(Product product, int quantity, ShippingMethod shippingMethod) {
        final PriceData priceData = calculatePriceData(product, quantity);
        return applyShipping(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();
        final PriceData priceData = new PriceData(basePrice, discount, quantity);
        return priceData;
    }

    private double applyShipping(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;
    }
}

PriceData

public record PriceData(double basePrice, double discount, int quantity) {
}

수정한 부분과 새로 작성한 코드 이외에는 모두 동일하다.

설명

  • PriceOrder() 함수의 내부 코드에서 서로 다른 일을 하는 코드를 함수로 분리하였다.
  • 또한 함수의 추상화 단계를 하나로 하여 priceOrder() 함수는 하나의 일을 하는 함수가 되었다.


함수 옮기기

Move Function

  • 모듈화가 잘 된 소프트웨어는 최소한의 지식만으로 프로그램을 변경할 수 있다.
  • 관련있는 함수나 필드가 모여있어야 더 쉽게 찾고 이해할 수 있다.
  • 하지만 관련있는 함수나 필드가 항상 고정적인 것은 아니기 때문에 때에 따라 옮겨야 할 필요가 있다.
  • 함수를 옮겨야 하는 경우
    • 해당 함수가 다른 문맥 (클래스)에 있는 데이터 (필드)를 더 많이 참조하는 경우.
    • 해당 함수를 다른 클라이언트 (클래스)에서도 필요로 하는 경우.
  • 함수를 옮겨갈 새로운 문맥 (클래스)이 필요한 경우에는 “여러 함수를 클래스로 묶기 (Combine Functions info Class)” 또는 “클래스 추출하기 (Extract Class)”를 사용한다.
  • 함수를 옮길 적당한 위치를 찾기가 어렵다면, 그대로 두어도 괜찮다. 언제든 나중에 옮길 수 있다

정리!

  • 응집도가 높으면 관련 있는 함수나 필드가 하나의 클래스에 모여있다.
  • 응집도를 높이기 위해 함수를 옮겨보자!
  • 외부 클래스에 대해 더 많은 참조가 있다면 함수를 옮기자.
  • 다른 클래스에서도 동일하게 필요로 하다면 새로운 클래스를 만들자.
  • 함수를 옮길 위치를 찾기가 어렵다면, 언제든 나중에 옮길 수 있다.

예제 코드

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;
        }
    }
}

AccountType

public class AccountType {
    private boolean premium;

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

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

냄새

Account 클래스 overdraftCharge() 함수에서 AccountType 클래스에 대한 의존이 있다.

설명

해당 함수를 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.type.overdraftCharge(this.daysOverdrawn);
        }
        return result;
    }

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

}

AccountType

public class AccountType {
    private boolean premium;

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

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

    public 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;
        }
    }
}

설명

  • overdraftCharge() 함수를 AccountType 클래스로 옮겼다.
  • 이를 위해 인텔리제이에서 move instance method 기능을 사용하였다.
  • 반대로 overDraftCharge() 함수가 account 를 많이 참조한다면 옮기자!


클래스 추출하기

Extract Class

  • 클래스가 다루는 책임(Responsibility)이 많아질수록 클래스가 점차 커진다.
  • 클래스를 쪼개는 기준
    • 데이터나 메소드 중 일부가 매우 밀접한 관련이 있는 경우
    • 일부 데이터가 대부분 같이 바뀌는 경우
    • 데이터 또는 메소드 중 일부를 삭제한다면 어떻게 될 것인가?
  • 하위 클래스를 만들어 책임을 분산 시킬 수도 있다.

예제 코드

Person


public class Person {

    private String name;

    private String officeAreaCode;

    private String officeNumber;

    public String telephoneNumber() {
        return this.officeAreaCode + " " + this.officeNumber;
    }

    public String name() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String officeAreaCode() {
        return officeAreaCode;
    }

    public void setOfficeAreaCode(String officeAreaCode) {
        this.officeAreaCode = officeAreaCode;
    }

    public String officeNumber() {
        return officeNumber;
    }

    public void setOfficeNumber(String officeNumber) {
        this.officeNumber = officeNumber;
    }
}

냄새

Person 클래스에서 officeAreaCode, officeNumber 필드가 밀접한 관계를 가지고 있어 책임을 분리 시켜줄 필요가 있다.

해결

새로운 클래스를 만들어 책임을 분산 시킬 수 있다.

리팩토링 후

Person

public class Person {

    private TelePhoneNumber telePhoneNumber;
    private String name;

    public Person(TelePhoneNumber telePhoneNumber, String name) {
        this.telePhoneNumber = telePhoneNumber;
        this.name = name;
    }

    public String telephoneNumber() {
        return telePhoneNumber.toString();
    }

    public String name() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public TelePhoneNumber getTelePhoneNumber() {
        return telePhoneNumber;
    }
}

TelePhoneNumber

public class TelePhoneNumber {
    private String areaCode;
    private String number;

    public TelePhoneNumber(String areaCode, String number) {
        this.areaCode = areaCode;
        this.number = number;
    }

    public String getAreaCode() {
        return areaCode;
    }

    public void setAreaCode(String areaCode) {
        this.areaCode = areaCode;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    @Override
    public String toString() {
        return "TelePhoneNumber{" +
                "areaCode='" + areaCode + '\'' +
                ", number='" + number + '\'' +
                '}';
    }
}

설명

밀접한 officeAreaCode, officeNumber 두 필드를 필드로 하는 클래스를 만들어 책임을 분리하였다.

profile
공부한 것을 잊지 않기 위해, 고민했던 흔적을 남겨 성장하기 위해 글을 씁니다.

0개의 댓글