Clean Code

Java 리팩토링 - 함수 옮기기 (Move Function)

1. 알고자 하는 것

프로그램을 작성할 때 모듈화가 잘 된 소프트웨어는 최소한의 지식만으로 프로그램을 변경할 수 있다.

  • 즉, A 도메인의 기능을 변경하기 위해 최소한의 코드만을 살펴볼 수 있도록 되어 있다면 모듈화가 잘 되어있는 것이다.
  • 반대로, A 도메인의 기능을 변경하기 위해 여러 클래스, 여러 코드를 봐야 이해할 수 있다면 모듈화가 잘 되어있지 않은 것이다.
  • 비슷한 문맥의 코드가 응집되어 있지 않고 여러 곳에 흩어져 있고 (응집도가 낮음), 불필요하게 여러 문맥 간 서로 의존하고 있어 하나의 변경점에 대해서 의존성이 묶여있는 여러 코드가 함께 변경되어야 한다면 (의존성 높음) 모듈화가 잘 되어있지 않은 것이다.
  • 높은 모듈화를 통해 하나의 변경사항에 대해 최소한의 코드 파악과 최소한의 코드 수정으로 효율을 높일 수 있어야 한다. 

이러한 모듈화를 위한 하나의 리팩토링 기법을 알아본다.

 

  • 함수 옮기기 (Move Function)

 

 

2. 알게 된 것

  • 모듈화를 위해서는 관련있는 메서드나 필드를 한 곳으로 모아서 살펴보아야 할 코드의 범위를 최소화해야 한다.
  • 하지만 우리가 작성하는 소프트웨어는 관련있는 메서드와 필드가 언제나 고정되어 있지 않다.
    • 요구사항이 변경되고, 도메인이 확장될 수록 변경되고 이동한다.
  • 이러한 경우, 기존에는 모듈화가 잘 되어있다고 판단되었던 코드가 더 이상은 모듈화가 잘 되어있지 않게 될 수도 있다.
    • 해당 메서드가 다른 클래스에 있는 데이터를 더 많이 참조하게 되는 경우
    • 해당 메서드를 다른 클래스에서도 필요로 하는 경우
  • 이럴 때, 함수 옮기기 (Move Function)를 통해 끊임없이 변화하는 요구사항에 유연하게 대처하고 모듈화를 지속할 수 있다.
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()) { // AccountType에 속한 필드
            final int baseCharge = 10;
            if (this.daysOverdrawn <= 7) { // Account에 속한 필드
                return baseCharge;
            } else {
                return baseCharge + (this.daysOverdrawn - 7) * 0.85;
            }
        } else {
            return this.daysOverdrawn * 1.75;
        }
    }
}

 

  • Account 클래스 내 메서드 중 납부일이 지난 경우 추가요금을 계산하는 overdraftCharge() 메서드가 있다.
  • 해당 메서드는 자신이 가진 필드인 daysOverdrawn이라는 필드를 참조하기도 하지만, AccountType이 가진 필드인 isPremium 필드 역시 참조한다.
  • 자신이 가진 필드와 AccountType이 가진 필드를 비슷하게 참조하므로 Account에 있어도 될 메서드이지만, AccountType에 있는 필드를 참조하기도 하고, 추후 AccountType에 있는 필드를 추가로 참조할 여지가 있을 수 있으므로 AccountType으로 메서드를 옮기는 함수 옮기기 (Move Function)를 사용하도록 한다.
public class AccountType {
    private boolean premium;

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

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

    // Account 필드의 daysOverdrawn을 매개변수로 받아온다
    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;
        }
    }
}
  • AccountType으로 해당 메서드를 옮겨오면서 Account에 있는 daysOverdrawn 필드를 필요로 하게 된다.
  • 이 때, Account 객체를 매개변수로 넘겨주는 경우 AccountType에 불필요한 Account 의존성이 생겨 변경점이 많아진다.
  • 또한, Account 객체는 필드로 AccountType을 가지고 있어 순환 참조(Circular Reference)가 발생한다.
  • 따라서 daysOverdrawn 필드만을 매개변수로 넘겨받아 함수를 옮긴다.
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) {
            // AccountType의 overdraftCharge 메서드 호출
            result += this.type.overdraftCharge(this.daysOverdrawn);
        }
        return result;
    }

    private int daysOverdrawn() {
        return this.daysOverdrawn;
    }
}
  • Account에서는 AccountType의 overdraftCharge 메서드를 호출하는 형태로 변경한다.
  • 이렇게 된다면 추후 overdraftCharge 메서드를 사용하는 또 다른 클래스가 있을 경우 AccountType에서 해당 메서드를 호출하면 된다.
  • 만약 overdraftCharge 메서드에 Account 필드를 더 많이 참조하게 된다면 이 때에는 매개변수에 Account 자체를 넘겨주어 순환 참조 / 불필요 의존성을 만들어내기보다는 다시 Account 클래스에 overdraftCharge 메서드를 옮겨주도록 한다.
    • 이렇게 기능이 변경되면서 함수는 계속 옮겨가게 된다.
    • 이러한 변경 사항을 주기적으로 판단하고 리팩토링을 꾸준히 진행해야 한다.

 

 

 

3. 정리

  • 모듈화가 잘 된 소프트웨어는 최소한의 지식만으로 프로그램을 변경할 수 있다.
    • 응집도가 높은 코드는 변경 사항이 있을 때 한 곳의 코드만 보아도 이해할 수 있다.
    • 의존성이 낮은 코드는 변경 사항이 있을 때 한 곳의 코드만 변경해도 side effect가 없다.
  • 하지만 소프트웨어는 언제나 끊임없이 변경사항에 대한 요구가 있으므로 관련있는 메서드나 필드 역시 고정되어 있지 않다.
  • 즉, 기존에는 모듈화가 잘 되어있다고 판단되었던 코드가 변경 후에는 모듈화가 잘 되어있지 않을 수 있다.
    • 해당 메서드가 다른 클래스에 있는 데이터를 더 많이 참조하게 되는 경우
    • 해당 메서드를 다른 클래스에서도 필요로 하는 경우
  • 이럴 때, 메서드를 다른 클래스로 옮기는 함수 옮기기 (Move Function)를 통해 주기적인 변경사항에 대해 모듈화를 유지한다.
  • 함수를 옮길 때 의존성과 순환참조 등, 변경되는 참조 관계를 고려하여 적절하게 판단한다.
  • 추후 또 다른 변경사항에 대해서 다시 함수를 옮기는 것을 주기적으로 판단하고 꾸준히 모듈화를 위해 고민해야 한다.

 

 


Reference

강의 - 코딩으로 학습하는 리팩토링 (백기선 강사님)