본문 바로가기
Study/Refactoring

리팩터링 11

by irerin07 2023. 8. 29.
728x90

11장


API 리팩터링

11.1 질의 함수와 변경 함수 분리하기

배경

  • 우리는 외부에서 관찰할 수 있는 겉보기 부수효과가 전혀 없이 값을 반환해주는 함수를 추구해야 한다.
  • 이를 위해 “질의 함수(읽기 함수)는 모두 부수효과가 없어야 한다”는 규칙을 따르는 것이다. 물론 이 규칙이 절대적인것은 아니지만 분명 유용하다.
  • 만약 값을 반환하면서 부수효과도 가지는 함수가 있다면 상태를 변경하는 부분과 질의하는 부분의 분리를 시도해본다.

절차

  1. 대상 함수를 복제하고 질의 목적에 충실한 이름을 짓는다.
    1. 함수 내부를 살펴 무엇을 반환하는지 찾는다. 어떤 변수의 값을 반환한다면 그 변수 이름이 훌륭한 단초가 될 것이다.
  2. 새 질의 함수에서 부수효과를 모두 제거한다.
  3. 정적 검사를 수행한다.
  4. 원래 함수(변경 함수)를 호출하는 곳을 모두 찾아낸다. 호출하는 곳에서 반환 값을 사용한다면 질의 함수를 호출하도록 바꾸고, 원래 함수를 호출하는 코드를 바로 아래 줄에 새로 추가한다. 하나 수정할때마다 테스트한다.
  5. 원래 함수에서 질의 관련 코드를 제거한다.
  6. 테스트한다.

이 리팩터링을 마친 후에는 새로 만든 질의 함수와 원래 함수에 (정리해야 할) 중복이 남아 있을 수 있다.

예시

public static String alertForMiscreant(String[] people) {
        for (String p : people) {
            if (p.equals("Don")) {
                setOffAlarms();
                return "Don";
            }
            if (p.equals("John")) {
                setOffAlarms();
                return "John";
            }
        }
        return "";
    }
1. 함수를 복제하고 질의 목적에 맞는 이름짓기
public static String findMiscreant(String[] people) {
        for (String p : people) {
            if (p.equals("Don")) {
                setOffAlarms();
                return "Don";
            }
            if (p.equals("John")) {
                setOffAlarms();
                return "John";
            }
        }
        return "";
    }
2. 새 질의 함수에서 부수효과를 낳는 부분 제거
public static String findMiscreant(String[] people) {
        for (String p : people) {
            if (p.equals("Don")) {
                ~~setOffAlarms();~~
                return "Don";
            }
            if (p.equals("John")) {
                ~~setOffAlarms();~~
                return "John";
            }
        }
        return "";
    }
4. 원래 함수를 호출하는 곳을 모두 찾아서 새로운 질의 함수로 호출하도록 바꾸고, 이어서 원래의 변경 함수를 호출하는 코드를 바로 아래에 삽입한다.
String found = alertForMiscreant(people); // 이 코드를

String found = findMiscreant(people); //이렇게 바꾼다.
alertForMiscreant(people);
5. 원래의 변경 함수에서 질의 관련 코드를 없앤다.
public static void alertForMiscreant(String[] people) {
        for (String p : people) {
            if (p.equals("Don")) {
                setOffAlarms();
                return;
            }
            if (p.equals("John")) {
                setOffAlarms();
                return;
            }
        }
    }
더 가다듬기.
 변경 함수와 새 질의 함수에 중복코드가 많다. 이번 경우엔 변경 함수에서 질의 함수를 사용하도록 고치면 해결된다. (알고리즘 교체하기 적용)
public static void alertForMiscreant(String[] people) {
        if (!findMiscreant(people).isEmpty()) {
            setOffAlarms();
        }
    }

11.2 함수 매개변수화하기

배경

  • 로직이 아주 비슷하고 단지 리터럴 값만 다르다면, 그 다른 값만 매개변수로 받아 처리하는 함수 하나로 합쳐 중복을 없앨 수 있다.

절차

  1. 비슷한 함수 중 하나를 선택한다.
  2. 함수 선언 바꾸기로 리터럴들을 매개변수로 추가한다.
  3. 이 함수를 호출하는 곳 모두에 적절한 리터럴 값을 추가한다.
  4. 테스트한다.
  5. 매개변수로 받은 값을 사용하도록 함수 본문을 수정한다. 하나 수정할 때마다 테스트한다.
  6. 비슷한 다른 함수를 호출하는 코드를 찾아 매개변수화된 함수를 호출하도록 하나씩 수정한다. 하나 수정할 때마다 테스트한다.
    1. 매개변수화된 함수가 대체할 비슷한 함수와 다르게 동작한다면, 그 비슷한 함수의 동작도 처리할 수 있도록 본문 코드를 적절히 수정한 후 진행한다.

예시

public static void tenPercentRaise(Person aPerson) {
        BigDecimal newSalary = aPerson.getSalary().multiply(new BigDecimal("1.10"));
        aPerson.setSalary(newSalary);
    }

    public static void fivePercentRaise(Person aPerson) {
        BigDecimal newSalary = aPerson.getSalary().multiply(new BigDecimal("1.05"));
        aPerson.setSalary(newSalary);
    }
위의 두 함수는 다음 함수로 대체할 수 있다.
public static void raise(Person aPerson, double factor) {
        BigDecimal newSalary = aPerson.getSalary().multiply(BigDecimal.valueOf(1 + factor));
        aPerson.setSalary(newSalary);
    }
public static double baseCharge(int usage) {
        if (usage < 0) return 0.0;

        double amount = bottomBand(usage) * 0.03
                      + middleBand(usage) * 0.05
                      + topBand(usage) * 0.07;

        return amount;
    }

    public static int bottomBand(int usage) {
        return Math.min(usage, 100);
    }

    public static int middleBand(int usage) {
        return usage > 100 ? Math.min(usage, 200) - 100 : 0;
    }

    public static int topBand(int usage) {
        return usage > 200 ? usage - 200 : 0;
    }

대역을 다루는 세 함수의 로직이 상당히 비슷한 건 사실이지만, 과연 매개변수화 함수로 통합할 수 있을 만큼 비슷한가? 그렇다. 하지만 앞의 간단한 예보다는 덜 직관적이다.

1. 비슷한 함수들을 매개변수화 하여 통합할때는 우선 대상 함수 중 하나를 골라 매개변수를 추가한다. 우선 middleBand()부터 시작한다.
2. middleBand() 는 리터럴 두 개 (100, 200) 사용하며, 그 각각은 중간 대역의 하한과 상한을 뜻한다. 함수 선언 바꾸기를 적용한다.
3. 이 리터럴들을 호출 시점에 입력하도록 바꿔보자. 이 과정에서 함수 이름도 매개변수화된 기능에 어울리게 수정한다.

public static double withinBand(int usage, int bottom, int top) {
        return usage > 100 ? Math.min(usage, 200) - 100 : 0;
    }

    public static double baseCharge(int usage) {
        if (usage < 0) return 0.0;

        double amount = bottomBand(usage) * 0.03
                      + withinBand(usage, 100, 200) * 0.05
                      + topBand(usage) * 0.07;

        return amount;
    }
5. 함수에서 사용하던 리터럴들을 적절한 매개변수로 대체한다.
public static double withinBand(int usage, int bottom, int top) {
        return usage > bottom ? Math.min(usage, top) - bottom : 0;
    }
나머지 매개변수도 대체한다.
public static double withinBand(int usage, int bottom, int top) {
        return usage > bottom ? Math.min(usage, top) - bottom : 0;
    }
6. 대역의 하한을 호출하는 부분도 새로 만든 매개변수화 함수를 호출하도록 바꾼다
public static double baseCharge(int usage) {
        if (usage < 0) return 0.0;

        double amount = withinBand(usage, 0, 100) * 0.03
                      + withinBand(usage, 100, 200) * 0.05
                      + topBand(usage) * 0.07;

        return amount;
    }

    ~~public static int bottomBand(int usage) {
        return Math.min(usage, 100);
    }~~
대역의 상한 호출을 대체할 때는 무한대를 뜻하는 Infinity를 이용했다.
public static double baseCharge(int usage) {
        if (usage < 0) return 0.0;

        double amount = withinBand(usage, 0, 100) * 0.03
                      + withinBand(usage, 100, 200) * 0.05
                      + withinBand(usage, 200, Integer.MAX_VALUE) * 0.07;

        return amount;
    }

~~public static int topBand(int usage) {
        return usage > 200 ? usage - 200 : 0;
    }~~

11.3 플래그 인수 제거하기

배경

  • 플래그 인수란 호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수다.
  • 플래그 함수를 사용하게 되면 호출할 수 있는 함수들이 무엇인지, 어떻게 호출해야 하는지 이해하기 난해해진다.
  • 이들은 함수들의 기능 차이를 잘 드러내지 않게 만든다.
  • 플래그 인수가 되려면 호출하는 쪽에서 불리언 값으로 (프로그램에서 사용되는 데이터가 아닌) 리터럴 값을 건네야 하고, 호출되는 함수는 그 인수를 (다른 함수에 전달하는 데이터가 아닌) 제어 흐름을 결정하는 데 사용해야 한다.
  • 함수 하나에 플래그 인수를 두 개 이상 사용한다면 이는 함수 하나가 너무 많은 일을 처리하고 있다는 신호일 수 있다.

절차

  1. 매개변수로 주어질 수 있는 값 각각에 대응하는 명시적 함수들을 생성한다.
    1. 주가 되는 함수에 깔끔한 분배 조건문이 포함되어 있다면 조건문 분해하기로 명시적 함수들을 생성하자. 그렇지 않다면 래핑 함수 형태로 만든다.
  2. 원래 함수를 호출하는 코드들을 모두 찾아서 각 리터럴 값에 대응되는 명시적 함수를 호출하도록 수정한다.

예시

public static LocalDate deliveryDate(Order anOrder, boolean isRush) {
        if (isRush) {
            int deliveryTime;
            if (Arrays.asList("MA", "CT").contains(anOrder.deliveryState)) {
                deliveryTime = 1;
            } else if (Arrays.asList("NY", "NH").contains(anOrder.deliveryState)) {
                deliveryTime = 2;
            } else {
                deliveryTime = 3;
            }
            return anOrder.placedOn.plusDays(1 + deliveryTime);
        } else {
            int deliveryTime;
            if (Arrays.asList("MA", "CT", "NY").contains(anOrder.deliveryState)) {
                deliveryTime = 2;
            } else if (Arrays.asList("ME", "NH").contains(anOrder.deliveryState)) {
                deliveryTime = 3;
            } else {
                deliveryTime = 4;
            }
            return anOrder.placedOn.plusDays(2 + deliveryTime);
        }
    }
aShipment.deliveryDate = deliveryDate(anOrder, true);
aShipment.deliveryDate = deliveryDate(anOrder, false);

호출하는 쪽에서는 이 불리언 리터럴 값을 이용해 어느 코드를 실행할지 정한다. → 플래그 인수

1. 이 예에서라면 조건문 분해하기를 적용한다.
public static LocalDate deliveryDate(Order anOrder, boolean isRush) {
        if (isRush) {
            return rushDeliveryDate(anOrder);
        } else {
            return regularDeliveryDate(anOrder);
        }
    }

    public static LocalDate rushDeliveryDate(Order anOrder) {
        int deliveryTime;
        if (Arrays.asList("MA", "CT").contains(anOrder.deliveryState)) {
            deliveryTime = 1;
        } else if (Arrays.asList("NY", "NH").contains(anOrder.deliveryState)) {
            deliveryTime = 2;
        } else {
            deliveryTime = 3;
        }
        return anOrder.placedOn.plusDays(1 + deliveryTime);
    }

    public static LocalDate regularDeliveryDate(Order anOrder) {
        int deliveryTime;
        if (Arrays.asList("MA", "CT", "NY").contains(anOrder.deliveryState)) {
            deliveryTime = 2;
        } else if (Arrays.asList("ME", "NH").contains(anOrder.deliveryState)) {
            deliveryTime = 3;
        } else {
            deliveryTime = 4;
        }
        return anOrder.placedOn.plusDays(2 + deliveryTime);
    }
2. 다음 호출은
aShipment.deliveryDate = deliveryDate(anOrder, true);

이렇게 대체할 수 있다.
aShipment.deliveryDate = rushDeliveryDate(anOrder);

모든 호출을 대체했다면 deliveryDate()를 제거한다.

매개변수를 까다로운 방식으로 사용할 때

public static LocalDate deliveryDate(Order anOrder, boolean isRush) {
        LocalDate result;
        int deliveryTime;

        if (anOrder.deliveryState.equals("MA") || anOrder.deliveryState.equals("CT")) {
            deliveryTime = isRush ? 1 : 2;
        } else if (anOrder.deliveryState.equals("NY") || anOrder.deliveryState.equals("NH")) {
            deliveryTime = 2;
            if (anOrder.deliveryState.equals("NH") && !isRush) {
                deliveryTime = 3;
            }
        } else if (isRush) {
            deliveryTime = 3;
        } else if (anOrder.deliveryState.equals("ME")) {
            deliveryTime = 3;
        } else {
            deliveryTime = 4;
        }

        result = anOrder.placedOn.plusDays(2 + deliveryTime);
        if (isRush) {
            result = result.minusDays(1);
        }
        return result;
    }

위 코드에서 isRuch를 최상위 분배 조건으로 뽑아내려면 생각보다 일이 커질 수 있다.

그렇다면 deliveryDate()를 감싸는 래핑 함수를 생각해볼 수 있다.

public static LocalDate rushDeliveryDate(Order anOrder) {
        return deliveryDate(anOrder, true);
    }

    public static LocalDate regularDeliveryDate(Order anOrder) {
        return deliveryDate(anOrder, false);
    }

이 두 함수를 추가했다면 호출하는 코드들을 앞에서 조건문을 쪼갰을 떄와 똑같은 방식으로 대체할 수 있다.

11.4 객체 통째로 넘기기

배경

  • 레코드를 통째로 넘기면 변화에 대응하기 쉽다.
  • 다만 함수가 레코드 자체에 의존하기를 원하지 않는 경우 해당 리팩터링을 수행하지 않는다.

절차

  1. 매개변수들을 원하는 형태로 받는 빈 함수를 만든다
    1. 마지막 단계에서 이 함수의 이름을 변경해야 하니 검색하기 쉬운 이름으로 지어준다.
  2. 새 함수의 본문에서는 원래 함수를 호출하도록 하며, 새 매개변수와 원래 함수의 매개변수를 매핑한다.
  3. 정적 검사를 수행한다.
  4. 모든 호출자가 새 함수를 사용하게 수정한다. 하나씩 수정하며 테스트한다.
    1. 수정 후에는 원래의 매개변수를 만들어내는 코드 일부가 필요 없어질 수 있다. 따라서 죽은 코드 제거하기로 없앨 수 있을 것이다.
  5. 호출자를 모두 수정했다면 원래 함수를 인라인 한다.
  6. 새 함수의 이름을 적절히 수정하고 모든 호출자에 반영한다.

예시

int low = aRoom.daysTempRange.low;
int high = aRoom.daysTempRange.high;
if (!aPlan.withinRange(low, high)) {
    alerts.add("room temperature went outside range");
}
boolean withinRange(int bottom, int top) {
    return (bottom >= this._temperatureRange.low) && (top <= this._temperatureRange.high);
}
1. 가장 먼저 원하는 인터페이스를 갖춘 빈 메서드를 만든다.
public boolean xxNEWwithinRange(NumberRange aNumberRange) {
    // Method body will be added here
}
2. 그런 다음 새 메서드의 본문은 기존 withinRange()를 호출하는 코드로 채운다. 자연스럽게 새 매개변수를 기존 매개변수와 매핑하는 로직이 만들어진다.
public boolean xxNEWwithinRange(NumberRange aNumberRange) {
    return this.withinRange(aNumberRange.low, aNumberRange.high);
}
4. 기존 함수를 호출하는 코드를 찾아서 새 함수를 호출하게 수정한다. 수정한 뒤 기존 코드 중 더는 필요 없는 부분은 죽은 코드이니 제거한다.
~~int low = aRoom.daysTempRange.low;
int high = aRoom.daysTempRange.high;~~
if (!aPlan.xxNEWwithinRange(aRoom.daysTempRange)) {
    alerts.add("room temperature went outside range");
}
5. 모두 새 함수로 대체했다면 원래 함수를 인라인 해준다.
public boolean xxNEWwithinRange(NumberRange aNumberRange) {
    return (aNumberRange.low >= this._temperatureRange.low) && (aNumberRange.high <= this._temperatureRange.high);
}
6. 마지막으로 새 함수에서 보기 흉한 접두어를 제거하고 호출자들에도 모두 반영한다. 접두어를 활용하면 이름 변경 기능을 제공하지 않는 코드 편집기를 사용하더라도 전체 바꾸기를 간단히 수행할 수 있다.
public boolean withinRange(NumberRange aNumberRange) {
    return (aNumberRange.low >= this._temperatureRange.low) && (aNumberRange.high <= this._temperatureRange.high);
}

if (!aPlan.withinRange(aRoom.daysTempRange)) {
    alerts.add("room temperature went outside range");
}

예시 : 새 함수를 다른 방식으로 만들기

  • 코드 작성 없이 순전히 다른 리팩터링들을 연달아 수행하여 새 메서드를 만들어내는 방법
int low = aRoom.daysTempRange.low;
int high = aRoom.daysTempRange.high;
if (!aPlan.withinRange(low, high)) {
    alerts.add("room temperature went outside range");
}

코드를 재정렬해서 기존 코드 일부를 메서드로 추출하는 방식으로 새 메서드를 만들려한다. 지금의 호출자 코드는 이에 적합하지 않지만 변수 추출하기를 몇 번 적용하면 원하는 모습으로 둔갑한다. 먼저, 조건문에서 기존 메서드를 호출하는 코드들을 해방시켜보자.

int low = aRoom.daysTempRange.low;
int high = aRoom.daysTempRange.high;
boolean isWithinRange = aPlan.withinRange(low, high);
if (!isWithinRange ) {
    alerts.add("room temperature went outside range");
}

그럼 다음 입력 매개변수를 추출한다.

TempRange tempRange = aRoom.days.getTempRange();
int low = tempRange.getLow();
int high = tempRange.getHigh();
boolean isWithinRange = aPlan.withinRange(low, high);
if (!isWithinRange ) {
    alerts.add("room temperature went outside range");
}

함수 추출하기로 새 메서드를 만들 수 있다.

TempRange tempRange = aRoom.days.getTempRange();
boolean isWithinRange = xxNEWwithinRange(aPlan, tempRange);
if (!isWithinRange ) {
    alerts.add("room temperature went outside range");
}
public boolean xxNEWwithinRange(HeantingPlan plan, TempRange tempRange) {
        int low = tempRange.getLow();
        int high = tempRange.getHigh();
        boolean isWithinRange = aPlan.withinRange(low, high);

    return isWithinRange;
}

원래 메서드는 다른 컨텍스트(HeantingPlan 클래스 안)에 있으니 함수 옮기기를 수행해야 한다.

TempRange tempRange = aRoom.days.getTempRange();
boolean isWithinRange = aPlan.xxNEWwithinRange(tempRange);
if (!isWithinRange ) {
    alerts.add("room temperature went outside range");
}
HeatingPlan 클래스
public boolean xxNEWwithinRange(NumberRange tempRange) {
    int low = tempRange.getLow();
    int high = tempRange.getHigh();
    boolean isWithinRange = this.withinRange(low, high);

    return isWithinRange;
}

11.5 매개변수를 질의 함수로 바꾸기

  • 반대 리팩터링 : 질의 함수를 매개변수로 바꾸기

배경

  • 피호출 함수가 스스로 쉽게 결정할 수 있는 값을 매개변수로 건네는 것 역시 중복이다. (굳이 매개변수로 건네받지 않아도 접근할 수 있는 값이 아닐까?)
  • 해당 매개변수를 제거하면 값을 결정하는 책임 주체가 달라진다
    • 매개변수가 있다면 결정 주체가 호출자가 된다.
    • 매개변수가 없다면 피호출 함수가 결정 주체가 된다.
  • 보통 호출하는 쪽을 간소하게 만들기 위해 책임 소재를 피호출 함수로 옮긴다. 물론 피호출 함수가 그 역할을 수행하기에 적합할 때만!
  • 만약 매개변수를 제거했을때 피호출 함수에 원치 않는 의존성이 생기는 경우 매개변수를 질의 함수로 바꾸지 말아야 한다.
  • 제거하려는 매개변수의 값을 다른 매개변수에서 질의해서 얻을 수 있다면 질의 함수로 바꿀 수 있다.
  • 주의사항으로는 대상 함수가 참조 투명해야 한다는 것이다.
    • 함수에 똑같은 값을 건네 호출하면 항상 똑같이 동작해야 한다는 것
  • 이런 특성은 테스트시 유용하므로 유지하도록 해야한다. 따라서 매개변수를 없애는 대신 가변 전역 변수를 이용하는 일은 하면 안된다.

절차

  1. 필요하다면 대상 매개변수의 값을 계산하는 코드를 변로 함수로 추출 해놓는다.
  2. 함수 본문에서 대상 매개변수로의 참조를 모두 찾아서 그 매개변수의 값을 만들어주는 표현식을 참조하도록 바꾼다. 하나 수정할 때마다 테스트한다.
  3. 함수 선언 바꾸기로 대상 매개변수를 없앤다.

예시

public class Order {

  private int quantity;
  private double itemPrice;

  public Order(int quantity, double itemPrice) {
    this.quantity = quantity;
    this.itemPrice = itemPrice;
  }

  public double getFinalPrice() {
    double basePrice = quantity * itemPrice;
    int discountLevel;

    if (quantity > 100) {
      discountLevel = 2;
    } else {
      discountLevel = 1;
    }

    return discountedPrice(basePrice, discountLevel);
  }

  private double discountedPrice(double basePrice, int discountLevel) {
    switch (discountLevel) {
      case 1:
        return basePrice * 0.95;
      case 2:
        return basePrice * 0.9;
      default:
        return basePrice;
    }
  }

}

임시 변수를 질의 함수로 바꾸기를 finalPrice() 함수에 적용한다.

public class Order {

  private int quantity;
  private double itemPrice;

  public Order(int quantity, double itemPrice) {
    this.quantity = quantity;
    this.itemPrice = itemPrice;
  }

  **public double getFinalPrice() {
    double basePrice = quantity * itemPrice;

    return discountedPrice(basePrice, discountLevel());
  }**

  **private int discountLevel() {
    return quantity > 100 ? 2 : 1;
  }**

  private double discountedPrice(double basePrice, int discountLevel) {
    switch (discountLevel) {
      case 1:
        return basePrice * 0.95;
      case 2:
        return basePrice * 0.9;
      default:
        return basePrice;
    }
  }

}

이제 discountedPrice() 함수에 discountLevel()의 반환 값을 건넬 이유가 사라졌다.

// 매개 변수를 참조하는 코드를 모두 함수 호출로 바꾼다.
public class Order {

  private int quantity;
  private double itemPrice;

  public Order(int quantity, double itemPrice) {
    this.quantity = quantity;
    this.itemPrice = itemPrice;
  }

  public double getFinalPrice() {
    double basePrice = quantity * itemPrice;

    return discountedPrice(basePrice, discountLevel());
  }

  private int discountLevel() {
    return quantity > 100 ? 2 : 1;
  }

  private double discountedPrice(double basePrice, int discountLevel) {
    **switch (discountLevel()) {**
      case 1:
        return basePrice * 0.95;
      case 2:
        return basePrice * 0.9;
      default:
        return basePrice;
    }
  }

}

함수 선언 바꾸기로 매개변수를 없앨 수 있다.

public class Order {

  private int quantity;
  private double itemPrice;

  public Order(int quantity, double itemPrice) {
    this.quantity = quantity;
    this.itemPrice = itemPrice;
  }

  public double getFinalPrice() {
    double basePrice = quantity * itemPrice;

    return discountedPrice(basePrice~~, discountLevel()~~);
  }

  private int discountLevel() {
    return quantity > 100 ? 2 : 1;
  }

  private double discountedPrice(double basePrice~~, int discountLevel~~) {
    switch (discountLevel()) {
      case 1:
        return basePrice * 0.95;
      case 2:
        return basePrice * 0.9;
      default:
        return basePrice;
    }
  }

}

11.6 질의 함수를 매개변수로 바꾸기

배경

  • 전역 변수를 참조하거나 제거하길 원하는 원소를 참조하는 경우 해당 참조를 매개변수로 바꿔 해결할 수 있다. 참조를 풀어내는 책임을 호출자로 옮기는 것
  • 이런 문제는 코드의 의존 관계를 바꾸려 할 때 벌어진다.
  • 똑같은 값을 건네면 매번 똑같은 결과를 내는 함수는 참조 투명성이라는 성질을 가지는데 참조 투명하지 않은 원소에 접근하는 모든 함수는 참조 투명성을 잃는다. 이는 해당 원소를 매개변수로 바꾸어 해결한다.
  • 모듈 개발시 순수 함수들을 따로 구분하고, 프로그램의 입출력과 기타 가변 원소들을 다루는 로직으로 순수 함수들의 겉을 감싸는 패턴을 많이 활용한다.
  • 이 리팩터링을 사용하여 프로그램의 일부를 순수 함수로 바꿀 수 있다.
  • 단점으로는 질의 함수를 매개변수로 바꾸면 어떤 값을 제공할지를 호출자가 알아내야 한다.

절차

  1. 변수 추출하기로 질의 코드를 함수 본문의 나머지 코드와 분리한다.
  2. 함수 본문 중 해당 질의를 호출하지 않는 코드들을 별도 함수로 추출한다.
    1. 이 함수의 이름은 나중에 수정해야 하니 검색하기 쉬운 이름으로 짓는다.
  3. 방금 만든 변수를 인라인하여 제거한다.
  4. 원래 함수도 인라인한다.
  5. 새 함수의 이름을 원래 함수의 이름으로 고쳐준다.

예시

public class HeatingPlan {

  private double maxTemperature;
  private double minTemperature;

  public HeatingPlan(double maxTemperature, double minTemperature) {
    this.maxTemperature = maxTemperature;
    this.minTemperature = minTemperature;
  }

  public double targetTemperature(Thermostat thermostat) {
    if (thermostat.getSelectedTemperature() > maxTemperature) {
      return maxTemperature;
    } else if (thermostat.getSelectedTemperature() < minTemperature){
      return minTemperature;
    }

    return thermostat.getSelectedTemperature();
  }

}

public class Thermostat {
  private double selectedTemperature;
  private double currentTemperature; // Added currentTemperature field

  public Thermostat(double selectedTemperature) {
    this.selectedTemperature = selectedTemperature;
  }

  public double getSelectedTemperature() {
    return selectedTemperature;
  }

  public double getCurrentTemperature() {
    return currentTemperature;
  }

  public void setCurrentTemperature(double currentTemperature) {
    this.currentTemperature = currentTemperature;
  }

  public void setToHeat() {
    // Code to set the thermostat to heating mode
  }

  public void setToCool() {
    // Code to set the thermostat to cooling mode
  }

  public void setOff() {
    // Code to turn off the thermostat
  }
}

public static void main(String[] args) {
    Thermostat thermostat = new Thermostat(72.0);
    HeatingPlan heatingPlan = new HeatingPlan(80.0, 20.0);
    double targetTemperature = heatingPlan.targetTemperature(thermostat);

    if (targetTemperature > thermostat.getCurrentTemperature()) {
      thermostat.setToHeat();
    } else if (targetTemperature < thermostat.getCurrentTemperature()) {
      thermostat.setToCool();
    } else {
      thermostat.setOff();
    }
  }

targetTemperature() 메서드가 전역 객체인 thermostat에 의존한다는 사실이 신경쓰인다. 이 전역 객체에 건네는 질의 메소드를 매개변수로 옮겨 의존성을 끊자.

// 변수 추출하기를 이용해여 이 메서드에서 사용할 매개변수를 준비한다.
public class HeatingPlan {

  private double maxTemperature;
  private double minTemperature;

  public HeatingPlan(double maxTemperature, double minTemperature) {
    this.maxTemperature = maxTemperature;
    this.minTemperature = minTemperature;
  }

  **public double targetTemperature(Thermostat thermostat) {
    double selectedTemperature = thermostat.getSelectedTemperature();

    if (selectedTemperature > maxTemperature) {
      return maxTemperature;
    } else if (selectedTemperature < minTemperature){
      return minTemperature;
    }

    return selectedTemperature;
  }**
}
// 매개변수의 값을 구하는 코드를 제외한 나머지를 메서드로 추출하기가 한결 수월해진다.
public class HeatingPlan {

  private double maxTemperature;
  private double minTemperature;

  public HeatingPlan(double maxTemperature, double minTemperature) {
    this.maxTemperature = maxTemperature;
    this.minTemperature = minTemperature;
  }

  public double targetTemperature(Thermostat thermostat) {
    if (thermostat.getSelectedTemperature() > maxTemperature) {
      return maxTemperature;
    } else if (thermostat.getSelectedTemperature() < minTemperature){
      return minTemperature;
    }

    return thermostat.getSelectedTemperature();
  }

  **public double xxNEWtargetTemperature(double selectedTemperature) {
    if (selectedTemperature > maxTemperature) {
      return maxTemperature;
    } else if (selectedTemperature < minTemperature) {
      return minTemperature
    }

    return selectedTemperature;
  }**

}
// 다음으로 방금 추출한 변수를 인라인하면 원래 메서드에는 단순한 호출만 남는다.
public class HeatingPlan {

  private double maxTemperature;
  private double minTemperature;

  public HeatingPlan(double maxTemperature, double minTemperature) {
    this.maxTemperature = maxTemperature;
    this.minTemperature = minTemperature;
  }

  **public double targetTemperature(Thermostat thermostat) {
    return xxNEWtargetTemperature(thermostat.getSelectedTemperature());
  }**

  public double xxNEWtargetTemperature(double selectedTemperature) {
    if (selectedTemperature > maxTemperature) {
      return maxTemperature;
    } else if (selectedTemperature < minTemperature) {
      return minTemperature;
    }

    return selectedTemperature;
  }

}
// 이어서 이 메서드까지 인라인 한다
public static void main(String[] args) {
  Thermostat thermostat = new Thermostat(72.0);
  HeatingPlan heatingPlan = new HeatingPlan(80.0, 20.0);

  **if (heatingPlan.xxNEWtargetTemperature(thermostat.getSelectedTemperature()) > thermostat.getCurrentTemperature()) {
    thermostat.setToHeat();
  } else if (heatingPlan.xxNEWtargetTemperature(thermostat.getSelectedTemperature()) < thermostat.getCurrentTemperature()) {
    thermostat.setToCool();
  } else {
    thermostat.setOff();
  }**
}
// 새 메서드의 이름을 원래 메서드의 이름으로 바꿀 차례다. 앞서 이 메서드의 이름을 검색하기 쉽게 만들어놓은 덕에 쉽게 바꿀 수 있다.
public static void main(String[] args) {
    Thermostat thermostat = new Thermostat(72.0);
    HeatingPlan heatingPlan = new HeatingPlan(80.0, 20.0);

    **if (heatingPlan.targetTemperature(thermostat.getSelectedTemperature()) > thermostat.getCurrentTemperature()) {
      thermostat.setToHeat();
    } else if (heatingPlan.targetTemperature(thermostat.getSelectedTemperature()) < thermostat.getCurrentTemperature()) {
      thermostat.setToCool();
    } else {
      thermostat.setOff();**
    }
  }

public class HeatingPlan {

  private double maxTemperature;
  private double minTemperature;

  public HeatingPlan(double maxTemperature, double minTemperature) {
    this.maxTemperature = maxTemperature;
    this.minTemperature = minTemperature;
  }

  ~~public double targetTemperature(Thermostat thermostat) {
    return xxNEWtargetTemperature(thermostat.getSelectedTemperature());
  }~~

  **public double targetTemperature(double selectedTemperature) {
    if (selectedTemperature > maxTemperature) {
      return maxTemperature;
    } else if (selectedTemperature < minTemperature) {
      return minTemperature;
    }

    return selectedTemperature;
  }**

}

11.7 세터 제거하기

배경

  • 세터 제거하기가 필요한 상황은 주로 두가지가 있다.
    1. 사람들이 무조건 접근자 메서드를 통해서만 필드를 다루려 할 때, 생성자를 제거해 객체가 생성된 이후에는 변경이 되서는 안된다는 것을 분명히 한다.
    2. 클라이언트에서 생성 스크립트를 사용해 객체를 생성할 때. 스크립트가 완료된 뒤로는 그 객체의 필드는 변경되지 않으리라 생각하기 때문에 이 의도를 명확하게 하기 위해 세터를 제거한다.

절차

  1. 설정해야 할 값을 생성자에서 받지 않는다면 그 값을 받을 매개변수를 생성자에 추가한다. 그런 다음 생성자 안에서 적절한 세터를 호출한다.
    1. 세터 여러 개를 제거하려면 해당 값 모두를 한꺼번에 생성자에 추가한다. 그러면 이후 과정이 간소해진다.
  2. 생성자 밖에서 세터를 호출하는 곳을 찾아 제거하고, 대신 새로운 생성자를 사용하도록 한다. 하나 수정할 때마다 테스트한다.
    1. 갱신하려는 대상이 공유 참조 객체라서 새로운 객체를 생성하는 방식으로는 세터 호출을 대체할 수 없다면 이 리팩터링을 취소한다.
  3. 세터 메소드를 인라인한다. 가능하다면 해당 필드를 불변으로 만든다.
  4. 테스트한다.

예시

public class Person {

  private String name;
  private String id;

  public Person() {
  }

  public String getName() {
    return name;
  }

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

  public String getId() {
    return id;
  }

  public void setId(String id) {
    this.id = id;
  }
}

public static void main(String[] args) {
    Person martin = new Person();

    martin.setName("martin");
    martin.setId("1234");
  }

이름은 추후에 변경 될 가능성이 있지만 id는 변경되어서는 안된다. 이 의도를 명확히 하기 위해 setter를 제거한다.

// 최초 한 번은 ID를 설정할 수 있어야 하니 함수 선언 바꾸기로 생성자에서 ID를 받도록 한다.
public class Person {

  private String name;
  private String id;

  public Person() {
  }

  **public Person(String id) {
    this.id = id;
  }**

  public String getName() {
    return name;
  }

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

  public String getId() {
    return id;
  }

  public void setId(String id) {
    this.id = id;
  }
}
// 그런 다음 생성 스크립트가 이 생성자를 통해 ID를 설정하게끔 수정한다.
public static void main(String[] args) {
    **Person martin = new Person("1234");**

    martin.setName("martin");
    ~~martin.setId("1234");~~
  }
// 모두 수정했다면 세터 메서드를 인라인 한다.
public class Person {

  private String name;
  private String id;

  public Person() {
  }

  public Person(String id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

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

  public String getId() {
    return id;
  }

  ~~public void setId(String id) {
    this.id = id;
  }~~

}

11.8 생성자를 팩터리 함수로 바꾸기

배경

  • 생성자와는 다르게 팩터리 함수에는 제약이 별로 없다.
  • 팩터리 함수를 구현하는 과정에서 생성자를 호출할 수는 있지만, 원한다면 다른 무언가로 대체할 수 있다.

절차

  1. 팩터리 함수를 만든다. 팩터리 함수의 본문에서는 원래의 생성자를 호출한다.
  2. 생성자를 호출하던 코드를 팩터리 함수 호출로 바꾼다.
  3. 하나씩 수정할 떄마다 테스트한다.
  4. 생성자의 가시 범위가 최소가 되도록 제한한다.

예시

public class Employee {
  private String name;
  private int typeCode;

  public Employee(String name, int typeCode) {
    this.name = name;
    this.typeCode = typeCode;
  }

  public String getName() {
    return name;
  }

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

  public int getTypeCode() {
    return typeCode;
  }

  public void setTypeCode(int typeCode) {
    this.typeCode = typeCode;
  }

  public String getType() {
    return legalTypeCodes.get(typeCode);
  }

  public static Map<String, String> legalTypeCodes = Map.of(
    "E", "Engineer",
    "M", "Manager",
    "S", "Salesman"
  );

}

public static void main(String[] args) {
    Employee candidate = new Employee(document.getName(), document.getEmpType());
    Employee leadEngineer = new Employee(document.getName(), "E");
}

우선 팩터리 함수를 만들어야 한다. 팩터리 본문은 단순히 생성자에 위임하는 방식으로 구현한다. (여기서 말하는 최상위는 어디…?)

public Employee createEmployee(String name, String typeCode) {
  return new Employee(name, typeCode);
}

그런 다음 생성자를 호출하는 곳을 찾아 수정한다. 생성자 대신 팩터리 함수를 사용하게 바꾼다.

Employee candidate = createEmployee(document.getName(), document.getEmpType());
Employee leadEngineer = createEmployee(document.getName(), "E");

이보다는 직원 유형을 팩터리 함수의 이름에 녹이는 방식을 권한다

Employee leadEngineer = createEngineer(document.getName());
public Employee createEngineer(String name) {
    return new Employee(name, "E");
}

11.9 함수를 명령으로 바꾸기

  • 반대 리팩터링 : 명령을 함수로 바꾸기

배경

  • 프로그래밍의 기본적인 빌딩 블록 중 하나인 함수를 해당 함수만을 위한 객체 안으로 캡슐화 하면 더 유용해진다. 이런 객체를 명령 객체라 한다.
  • 명령은 훨씬 유연하게 함수를 제어하고 표현할 수 있다.
  • 되돌리기 같은 보조 연산을 제공할 수 있다
  • 수명주기를 더 정밀하게 제어하는 데 필요한 매개변수를 만들어주는 메서드를 제공할 수 있다.
  • 상속과 훅을 이용해 사용자 맞춤형으로 만들 수 있다.
  • 객체는 지원하지만 일급 함수를 지원하지 않는 프로그래밍 언어를 사용할 때는 명령을 이용해 일급 함수의 기능 대부분을 흉내 낼 수 있다.
  • 중첩 함수를 지원하지 않는 언어에서도 메서드와 필드를 이용해 복잡한 함수를 잘게 쪼갤 수 있고, 이렇게 쪼갠 메서드들을 테스트와 디버깅에 직접 이용할 수 있다.
  • 유연성은 복잡성을 키우고 얻는 대가이기에 일급 함수와 명령 중 선택해야 한다면 가능한 일급 함수를 사용하자.
  • 명령보다 더 간단한 방식으로는 얻을 수 없는 기능이 필요할 때 명령을 사용한다.

절차

  1. 대항 함수의 기능을 옮긴 빈 클래스를 만든다. 클래스 이름은 함수 이름에 기초해 짓는다.
  2. 방금 생선한 빈 클래스로 함수를 옮긴다.
    1. 리팩터링이 끝날 때까지는 원래 함수를 전달 함수 역할로 남겨두자
    2. 명령 관련 이름은 사용하는 프로그래밍 언어의 명명규칙을 따른다.
  3. 함수의 인수들 각각은 명령의 필드로 만들어 생성자를 통해 설정할지 고민해본다.

예시

public class Score {
  public static int score(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
    int result = 0;
    int healthLevel = 0;
    boolean highMedicalRiskFlag = false;

    if (medicalExam.isSmoker()) {
      healthLevel += 10;
      highMedicalRiskFlag = true;
    }

    String certificationGrade = "regular";
    if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
      certificationGrade = "low";
      result -= 5;
    }

    // lots more code like this
    result -= Math.max(healthLevel - 5, 0);
    return result;
  }

}

우선 빈 클래스를 만들고 해당하는 함수를 빈 클래스로 옮기는 일부터 진행한다.

public class Score {

  **public int score(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
    return new Scorer().execute(candidate, medicalExam, scoringGuide);
  }**

}
**public class Scorer {
  public int execute(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
    int result = 0;
    int healthLevel = 0;
    boolean highMedicalRiskFlag = false;

    if (medicalExam.isSmoker()) {
      healthLevel += 10;
      highMedicalRiskFlag = true;
    }

    String certificationGrade = "regular";
    if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
      certificationGrade = "low";
      result -= 5;
    }

    // lots more code like this
    result = Math.max(healthLevel - 5, 0);
    return result;
  }
}**

매개변수 옮기기는 한 번에 하나씩 수행한다.

public class Score {
  public int score(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
    return new Scorer(**candidate**).execute(~~candidate~~, medicalExam, scoringGuide);
  }

}
public class Scorer {
  **private Candidate candidate;

  public Scorer(Candidate candidate) {
    this.candidate = candidate;
  }**

  public int execute(~~Candidate candidate~~, MedicalExam medicalExam, ScoringGuide scoringGuide) {
    int result = 0;
    int healthLevel = 0;
    boolean highMedicalRiskFlag = false;

    if (medicalExam.isSmoker()) {
      healthLevel += 10;
      highMedicalRiskFlag = true;
    }

    String certificationGrade = "regular";
    if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
      certificationGrade = "low";
      result -= 5;
    }

    // lots more code like this
    result = Math.max(healthLevel - 5, 0);
    return result;
  }
}

다른 매개변수도 옮긴다.

public class Score {
  public int score(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
    return new Scorer(candidate, **medicalExam, scoringGuide**).execute(~~medicalExam, scoringGuide~~);
  }

}
public class Scorer {
  **private Candidate candidate;
  private MedicalExam medicalExam;
  private ScoringGuide scoringGuide;

  public Scorer(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
    this.candidate = candidate;
    this.medicalExam = medicalExam;
    this.scoringGuide = scoringGuide;
  }**

  public int execute(~~MedicalExam medicalExam, ScoringGuide scoringGuide~~) {
    int result = 0;
    int healthLevel = 0;
    boolean highMedicalRiskFlag = false;

    if (medicalExam.isSmoker()) {
      healthLevel += 10;
      highMedicalRiskFlag = true;
    }

    String certificationGrade = "regular";
    if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
      certificationGrade = "low";
      result -= 5;
    }

    // lots more code like this
    result = Math.max(healthLevel - 5, 0);
    return result;
  }
}

더 가다듬기

이 리팩터링의 본래 목적은 복잡한 함수를 잘게 나누는 것이다.

먼저 모든 지역 변수를 필드로 바꿔야 한다.

public class Scorer {
  private Candidate candidate;
  private MedicalExam medicalExam;
  private ScoringGuide scoringGuide;

  **private int result;**

  public Scorer(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
    this.candidate = candidate;
    this.medicalExam = medicalExam;
    this.scoringGuide = scoringGuide;
  }

  public int execute() {
    **result = 0;**
    int healthLevel = 0;
    boolean highMedicalRiskFlag = false;

    if (medicalExam.isSmoker()) {
      healthLevel += 10;
      highMedicalRiskFlag = true;
    }

    String certificationGrade = "regular";
    if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
      certificationGrade = "low";
      result -= 5;
    }

    // lots more code like this
    result = Math.max(healthLevel - 5, 0);
    return result;
  }
}

다른 지역 변수도 동일하게 바꿔준다.

public class Scorer {
  private Candidate candidate;
  private MedicalExam medicalExam;
  private ScoringGuide scoringGuide;

  private int result;
  **private int healthLevel;
  private boolean highMedicalRiskFlag;**

  public Scorer(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
    this.candidate = candidate;
    this.medicalExam = medicalExam;
    this.scoringGuide = scoringGuide;
  }

  public int execute() {
    result = 0;
    **healthLevel = 0;
    highMedicalRiskFlag = false;**

    if (medicalExam.isSmoker()) {
      healthLevel += 10;
      highMedicalRiskFlag = true;
    }

    String certificationGrade = "regular";
    if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
      certificationGrade = "low";
      result -= 5;
    }

    // lots more code like this
    result = Math.max(healthLevel - 5, 0);
    return result;
  }
}

이제 함수의 상태가 모두 명령 객체로 옮겨졌다. 따라서 함수가 사용하던 변수나 그 유효범위에 구애받지 않고 함수 추출하기 같은 리팩터링을 적용할 수 있다.

public class Scorer {
  private Candidate candidate;
  private MedicalExam medicalExam;
  private ScoringGuide scoringGuide;

  private int result;
  private int healthLevel;
  private boolean highMedicalRiskFlag;

  public Scorer(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
    this.candidate = candidate;
    this.medicalExam = medicalExam;
    this.scoringGuide = scoringGuide;
  }

  public int execute() {
    result = 0;
    healthLevel = 0;
    highMedicalRiskFlag = false;

    scoreSmoking();

    String certificationGrade = "regular";
    if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
      certificationGrade = "low";
      result -= 5;
    }

    // lots more code like this
    result = Math.max(healthLevel - 5, 0);
    return result;
  }

  public void scoreSmoking() {
    if (medicalExam.isSmoker()) {
      healthLevel += 10;
      highMedicalRiskFlag = true;
    }
  }
}

이제 명령을 중첩 함수처럼 다룰 수 있다.

11.10 명령을 함수로 바꾸기

배경

  • 명령은 그저 함수를 하나 호출해 정해진 일을 수행하는 용도로 주로 쓰인다. 이런 상황이고 로직이 크게 복잡하지 않다면 명령 객체는 장점보다 단점이 더 크니 평범한 함수로 바꿔준다.

절차

  1. 명령을 생성하는 코드와 명령의 실행 메서드를 호출하는 코드를 함께 함수로 추출한다.
    1. 이 함수가 바로 명령을 대체할 함수다
  2. 명령의 실행 함수가 호출하는 보조 메서드들 각각을 인라인 한다.
    1. 보조 메서드가 값을 반환한다면 함수 인라인에 앞서 변수 추출하기를 적용한다.
  3. 함수 선언 바꾸기를 적용하여 생성자의 매개변수 모두를 명령의 실행 메서드로 옮긴다.
  4. 명령의 실행 메서드에서 참조하는 필드들 대신 대응하는 매개변수를 사용하게끔 바꾼다. 하나씩 수정할 때마다 테스트한다.
  5. 생성자 호출과 명령의 실행 메서드 호출을 호출자(대체 함수)안으로 인라인한다.
  6. 테스트한다.
  7. 죽은 코드 제거하기로 명령 클래스를 없앤다.

예시

public class ChargeCalculator {

  private int usage;
  private Customer customer;
  private Provider provider;

  public ChargeCalculator(int usage, Customer customer, Provider provider) {
    this.usage = usage;
    this.customer = customer;
    this.provider = provider;
  }

  public int getBaseCharge() {
    return this.customer.getBaseRate() * usage;
  }

  public int getCharge() {
    return getBaseCharge() + provider.getConnectionCharge();
  }

}
public static void main(String[] args) {

    ChargeCalculator chargeCalculator = new ChargeCalculator(new Customer(), 10, new Provider());
    int charge = chargeCalculator.getCharge();

  }

이 명령 클래스는 간단한 편이므로 함수로 대체하는 게 나아 보인다.

// 첫 번째로, 이 클래스를 생성하고 호출하는 코드를 함께 함수로 추출한다.
public static void main(String[] args) {

    int monthCharge = charge(new Customer(), 10, new Provider());

  }

  public static int charge(Customer customer, int usage, Provider provider) {
    return new ChargeCalculator(customer, usage, provider).getCharge();
  }
// 이때 보조 메서드들을 어떻게 다룰지 정해야 하는데, baseCharge()가 이러한 보조 메서드에 속한다. 값을 반환하는 메서드라면 먼저 반환할 값을 변수로 추출한다.
public class ChargeCalculator {

  private Customer customer;
  private int usage;
  private Provider provider;

  public ChargeCalculator(Customer customer, int usage, Provider provider) {
    this.customer = customer;
    this.usage = usage;
    this.provider = provider;
  }

  public int getBaseCharge() {
    return this.customer.getBaseRate() * usage;
  }

  public int getCharge() {
    **int baseCharge = getBaseCharge();
    return baseCharge + provider.getConnectionCharge();**
  }

}
// 그럼 다음 보조 메서드를 인라인한다.
public class ChargeCalculator {

  private Customer customer;
  private int usage;
  private Provider provider;

  public ChargeCalculator(Customer customer, int usage, Provider provider) {
    this.customer = customer;
    this.usage = usage;
    this.provider = provider;
  }

  ~~public int getBaseCharge() {
    return customer.getBaseRate() * usage;
  }~~

  public int getCharge() {
    **int baseCharge = this.customer.getBaseRate() * usage;**
    return baseCharge + provider.getConnectionCharge();
  }

}

이제 로직 전체가 한 메서드에서 이뤄지므로, 다음으로는 생성자에 전달되는 모든 데이터를 주 메서드로 옮겨야 한다.

먼저 생성자가 받던 모든 매개변수를 charge()메서드로 옮기기 위해 함수 선언 바꾸기를 적용한다.

public class ChargeCalculator {

  private Customer customer;
  private int usage;
  private Provider provider;

  public ChargeCalculator(Customer customer, int usage, Provider provider) {
    this.customer = customer;
    this.usage = usage;
    this.provider = provider;
  }

  **public int getCharge(Customer customer, int usage, Provider provider) {**
    int baseCharge = this.customer.getBaseRate() * this.usage;
    return baseCharge + this.provider.getConnectionCharge();
  }

}
public static void main(String[] args) {

    int monthCharge = charge(new Customer(), 10, new Provider());

  }

  public static int charge(Customer customer, int usage, Provider provider) {
    **return new ChargeCalculator(customer, usage, provider).getCharge(customer, usage, provider);**
  }

이제 charge()의 본문에서 필드 대신 건네받은 매개변수를 사용하도록 수정한다.

public class ChargeCalculator {

  private Customer customer;
  private int usage;
  private Provider provider;

  public ChargeCalculator(Customer customer, int usage, Provider provider) {
    ~~this.customer = customer;~~
    this.usage = usage;
    this.provider = provider;
  }

  public int getCharge(Customer customer, int usage, Provider provider) {
    **int baseCharge = customer.getBaseRate() * usage;**
    return baseCharge + provider.getConnectionCharge();
  }

}

나머지 매개변수들도 똑같이 바꿔준다.

public class ChargeCalculator {

  ~~private Customer customer;
  private int usage;
  private Provider provider;~~

  ~~public ChargeCalculator(Customer customer, int usage, Provider provider) {
    this.customer = customer;
    this.usage = usage;
    this.provider = provider;
  }~~

    public ChargeCalculator() {
    }

  public int getCharge(Customer customer, int usage, Provider provider) {
    int baseCharge = customer.getBaseRate() * usage;
    return baseCharge + provider.getConnectionCharge();
  }

}

다 됐다면 최상위 charge() 함수로 인라인할 수 있다. 이는 생성자와 메서드 호출을 함께 인라인하는 특별한 형태의 함수 인라인하기다.

public static void main(String[] args) {

    int monthCharge = charge(new Customer(), 10, new Provider());

  }

  public static int charge(Customer customer, int usage, Provider provider) {
    **int baseCharge = customer.getBaseRate() * usage;
    return baseCharge + provider.getConnectionCharge();**
  }

명령 클래스는 이제 죽은 코드가 되었으니 죽은 코드 제거하기로 제거한다.

~~public class ChargeCalculator {

  public ChargeCalculator() {
  }

  public int getCharge(Customer customer, int usage, Provider provider) {
    int baseCharge = customer.getBaseRate() * usage;
    return baseCharge + provider.getConnectionCharge();
  }

}~~

11.11 수정된 값 반환하기

배경

  • 데이터가 수정된 경우 변수를 갱신하는 함수라면 수정된 값을 반환하여 호출자가 그 값을 변수에 담아두도록 한다. 이를 통해 호출자 코드를 읽을 때 변수가 갱신될 것임을 분명히 인지할 수 있다.
  • 값 하나를 계산한다는 목적이 있는 함수들에 효과적이고, 값 여러 개를 갱신하는 함수에는 효과적이지 못하다.
  • 함수 옮기기의 준비 작업으로 적용하기 좋은 리팩터링이다.

절차

  1. 함수가 수정된 값을 반환하게 하여 호출자가 그 값을 자신의 변수에 저장하게 한다.
  2. 테스트한다.
  3. 피호출 함수 안에 반환활 값을 가리키는 새로운 변수를 선언한다.
    1. 이 작업이 의도대로 이뤄졌는지 검사하고 싶다면 호출자에서 초깃값을 수정해보자. 제대로 처리했다면 수정된 값이 무시된다.
  4. 테스트한다.
  5. 계산이 선언과 동시에 이뤄지도록 통합한다.
  6. 테스트한다.
  7. 피호출 함수의 변수 이름을 새 역할에 어울리도록 바꿔준다.
  8. 테스트한다.

예시

public class GPSCalculator {

  private Point[] points;

  public void calculateByGPSLocation() {
    int totalAscent = 0;
    int totalTime = 0;
    int totalDistance = 0; 

    calculateAscent();
    calculateTime();
    calculateDistance();

    int pace = totalTime / 60 / totalDistance;
  }

  private void calculateDistance() {
    int totalAscent = 0;

    for (int i = 1; i < points.length; i++) {
      int verticalChange = points[i].getElevation() - points[i-1].getElevation();
      totalAscent += (verticalChange > 0) ? verticalChange : 0;
    }

  }

  private void calculateTime() {
  }

  private void calculateAscent() {
  }

}

현재는 calculateAscent() 안에서 totalAscent가 갱신된다는 사실이 드러나지 않으므로 calculateAscent()와 외부 환경이 어떻게 연결돼 있는지가 숨겨진다. 갱신 사실을 밖으로 알려보자

// 우선 totalAscent 값을 반환하고, 호출한 곳에서 변수에 대입하게 고친다.
public class GPSCalculator {

  private Point[] points;

  public void calculateByGPSLocation() {
    int totalAscent = 0;
    int totalTime = 0;
    int totalDistance = 0;

    **totalAscent = calculateAscent();**
    calculateTime();
    calculateDistance();

    int pace = totalTime / 60 / totalDistance;
  }

  private int calculateAscent() {
    int totalAscent = 0;

    for (int i = 1; i < points.length; i++) {
      int verticalChange = points[i].getElevation() - points[i-1].getElevation();
      totalAscent += (verticalChange > 0) ? verticalChange : 0;
    }

    **return totalAscent;**
  }

  private void calculateTime() {
  }

  private void calculateDistance() {
  }

}

그런 다음 calcuateAscent() 안에 반환할 값을 담을 변수인 totalAscent를 선언한다. 그런데 이 결과로 부모 코드에 있는 똑같은 이름의 변수가 가려진다.

이를 피하기 위해 명명규칙에 맞게 수정한다.

public class GPSCalculator {

  private Point[] points;

  public void calculateByGPSLocation() {
    int totalAscent = 0;
    int totalTime = 0;
    int totalDistance = 0;

    totalAscent = calculateAscent();
    calculateTime();
    calculateDistance();

    int pace = totalTime / 60 / totalDistance;
  }

  private int calculateAscent() {
    **int result = 0;**

    for (int i = 1; i < points.length; i++) {
      int verticalChange = points[i].getElevation() - points[i-1].getElevation();
      **result += (verticalChange > 0) ? verticalChange : 0;**
    }

    **return result;**
  }

  private void calculateTime() {
  }

  private void calculateDistance() {
  }

}

그런 다음 이 계산이 변수 선언과 동시에 수해오디도록 하고, 불변으로 만든다. (왜 불변으로 만들어야 하지? 값이 단 한 번만 정해지면 된다는 것을 보여주기 위해?)

public class GPSCalculator {

  private Point[] points;

  public void calculateByGPSLocation() {
    **final int totalAscent = calculateAscent();**
    int totalTime = 0;
    int totalDistance = 0;

    calculateTime();
    calculateDistance();

    int pace = totalTime / 60 / totalDistance;
  }

  private int calculateAscent() {
    int result = 0;

    for (int i = 1; i < points.length; i++) {
      int verticalChange = points[i].getElevation() - points[i-1].getElevation();
      result += (verticalChange > 0) ? verticalChange : 0;
    }

    return result;
  }

  private void calculateTime() {
  }

  private void calculateDistance() {
  }

}

11.12 오류 코드를 예외로 바꾸기

배경

  • 예외는 다른 정교한 메커니즘과 함께 정확하게 사용할 때만 최고의 효과를 낸다. 정확히 예상 밖의 동작일 때만 쓰여야 한다.
  • 예외를 던지는 코드를 프로그램 종료 코드로 바꿔도 프로그램이 여전히 정상 동작할지를 따져서 정상 동작하지 않을 것 같다면 예외를 사용하지 말라는 신호다.
    • 이런 경우에는 예외 대신 오류를 검출하여 프로그램을 정상 흐름으로 되돌리게끔 처리해야 한다.

절차

  1. 콜스택 상위에 해당 예외를 처리할 예외 핸들러를 작성한다.
    1. 이 핸들러는 처음에는 모든 예외를 다시 던지게 해둔다.
    2. 적절한 처리를 해주는 핸들러가 이미 있다면 지금의 콜스택도 처리할 수 있도록 확장한다.
  2. 테스트한다.
  3. 해당 오류 코드를 대체할 예외와 그 밖의 예외를 구분할 식별 방법을 찾는다.
    1. 사용하는 프로그래밍 언어에 맞게 선택하면 된다. 대부분 언어에서는 서브클래스를 사용하면 될것이다.
  4. 정적 검사를 수행한다.
  5. catch절을 수정하여 직접 처리할 수 있는 예외는 적절히 대처하고 그렇지 않은 예외는 다시 던진다.
  6. 테스트한다.
  7. 오류 코드를 반환하는 곳 모두에서 예외를 던지도록 수정한다. 하나씩 수정할 때마다 테스트한다.
  8. 모두 수정했다면 그 오류 코드를 콜스택 위로 전달하는 코드를 모두 제거한다. 하나씩 수정할 때마다 테스트한다.
    1. 먼저 오류 코드를 검사하는 부분을 함정으로 바꾼 다음, 함정에 걸려들지 않는지 테스트한 수 제거하는 전략을 권한다. 함정에 걸려드는 곳이 있다면 오류 코드를 검사하는 코드가 아직 남아있다는 뜻이다. 함정을 무사히 피했다면 안심하고 본문을 정리하자

예시

public int localShippingRules(String country) {
    ShippingRulesData data = countryData.get(country);
    if (data != null) {
        return new ShippingRules(data).getShippingCost();
    } else {
        return -23;
    }
}

위 코드는 국가 정보(country)가 유효한지를 이 함수 호출 전에 다 검증했다고 가정한다. 그렇기에 이 함수에서 오류가 난다면 무언가 잘못되었다는 뜻이다.

// 다음과 같이 호출한 곳에서는 반환된 오류 코드를 검사하여 오류가 발견되면 위로 전파한다.
public int calculateShippingCosts(Order anOrder) {
        // Not related codes

        int shippingRules = localShippingRules(anOrder.getCountry());

        if (shippingRules < 0) {
            return shippingRules; //오류 전파
        }

        // Not related codes
    }

더 윗단 함수는 오류를 낸 주문을 오류 목록(errorList)에 넣는다.

int status = calculator.calculateShippingCosts(order);
if (status < 0) {
    errorList.add(new ErrorEntry(order, status));
}

여기서 가장 먼저 고려할 것은 이 오류가 ‘예상된 것이냐’다

만약 예상할 수 있는 정상 동작 범주에 든다면 오류 코드를 예외로 바꾸는 리팩터링을 적용할 준비가 된 것이다.

// 가장 먼저 최상위에 예외 핸들러를 갖춘다. 오류 처리 로직을 포함하고 싶지않다. 하지만 status를 확인하는 코드를 try catch 블록 바깥으로 위치하면 status의 유효범위가 try 블록으로 국한되어 검사할 수가 없다.
try {
    int status = calculator.calculateShippingCosts(order);
    if (status < 0) {
        errorList.add(new ErrorEntry(order, status));
    }
} catch (Exception e) {
    // Exception handling

}

status 선언과 초기화를 분리해야 한다. 지금은 어쩔 수 없이 진행하지만 평소라면 쓰지 않았을 것이다.

int status = 0;
status = calculateShippingCosts(orderData);
if (status <0) {
  errorList.add(new ErrorEntry(order, status));
}

이제 함수 호출을 try catch 블록으로 감싼다.

int status = 0;
try {
  status = calculateShippingCosts(orderData);
} catch (Exception e) {
  //exception handling
}

if (status <0) {
  errorList.add(new ErrorEntry(order, status));
}

이번 리팩터링으로 추가된 예외만을 처리하고자 한다면 다른 예외와 구별할 방법이 필요하다.

int status = 0;
try {
  status = calculateShippingCosts(orderData);
} catch (NullPointerException npe) {
  //exception handling
} catch (Exception e) {
    //exception handling
}

if (status <0) {
  errorList.add(new ErrorEntry(order, status));
}
public int localShippingRules(String country) {
    ShippingRulesData data = countryData.get(country);
    if (data != null) {
        return new ShippingRules(data).getShippingCost();
    } else {
        throw new OrderProcessingException(-23);
    }
}

코드를 다 작성했고 테스트도 통과했다면 오류 코드를 전파하는 임시 코드를 제거할 수 있지만 우선 함정을 추가한 후 테스트를 해본다.

함정에 걸려들지 않는다면 줄 전체를 제거해도 안전하다.

int status = calculator.calculateShippingCosts(order);
~~if (status < 0) {
    errorList.add(new ErrorEntry(order, status));
}~~
int status = 0;
try {
  status = calculateShippingCosts(orderData);
} catch (NullPointerException npe) {
  //exception handling
} catch (Exception e) {
    //exception handling
}

~~if (status <0) {
  errorList.add(new ErrorEntry(order, status));
}~~
try {
  ~~status =~~ calculateShippingCosts(orderData);
} catch (NullPointerException npe) {
  //different exception handling
} catch (Exception e) {
    //exception handling
}

11.13 예외를 사전확인으로 바꾸기

배경

  • 예외는 말 그래도 예외적으로 동작할 때만 쓰여야 한다.
  • 함수 수행 시 문제가 될 수 있는 조건을 함수 호출 전에 검사할 수 있다면, 예외를 던지는 대신 호출하는 곳에서 조건을 검사하도록 한다.

절차

  1. 예외를 유발하는 상황을 검사할 수 있는 조건문을 추가한다. catch 블록의 코드를 조건문의 조건절중 하나로 옮기고, 남은 try블록의 코드를 다른 조건절로 옮긴다.
  2. catch 블록에 어서션을 추가하고 테스트한다.
  3. try문과 catch 블록을 제거한다.
  4. 테스트한다.

예시

public class ResourcePool {
  public Resource get() {
    Resource result;
    try {
      result = available.pop();
      allocated.add(result);
    } catch (NoSuchElementException e) {
      result = Resource.create();
      allocated.add(result);
    }
    return result;
  }

  private Deque<Resource> available;
  private List<Resource> allocated;

}

풀에서 자원이 고갈되는 건 예상치 못한 조건이 아니므로 예외 처리로 대응하는 건 옳지 않다.

사용하기 전에 allocated 컬렉션의 상태를 확인하여 예상 범주에 있는 동작임을 뚜렷하게 드러내준다.

// 조건 검사 코드를 추가하고, catch 블록의 코드를 조건문의 조건절로 옮기고, 남은 try블록 코드를 다른 조건절로 옮긴다.
public class ResourcePool {
  public Resource get() {
    Resource result;

    **if (available.isEmpty()) {
      result = Resource.create();
      allocated.add(result);
    } else {
      try {
        result = available.pop();
        allocated.add(result);
      } catch (NoSuchElementException e) {
        result = Resource.create();
        allocated.add(result);
      }
    }**

    return result;
  }

  private Deque<Resource> available;
  private List<Resource> allocated;

}

catch 절은 더 이상 호출되지 않으므로 어서션을 추가한다.

public class ResourcePool {
  public Resource get() {
    Resource result;

    if (available.isEmpty()) {
      result = Resource.create();
      allocated.add(result);
    } else {
      try {
        result = available.pop();
        allocated.add(result);
      } catch (NoSuchElementException e) {
        **throw new AssertionError("도달 불가");**
      }
    }

    return result;
  }

  private Deque<Resource> available;
  private List<Resource> allocated;

}

어서션 추가 후 테스트에 통과하면 try catch 블록을 삭제한다.

public class ResourcePool {
  public Resource get() {
    Resource result;

    if (available.isEmpty()) {
      result = Resource.create();
      allocated.add(result);
    } else {
        result = available.pop();
        allocated.add(result);
    }

    return result;
  }

  private Deque<Resource> available;
  private List<Resource> allocated;

}

한 번 더 테스트에 통과하면 이번 리팩터링은 끝이다.

그런데 이번 리팩터링 결과로 얻어진 코드에는 정리할 거리가 더 있다.

// 우선 문장 슬라이드하기 부터 적용한다.
public class ResourcePool {
  public Resource get() {
    Resource result;

    if (available.isEmpty()) {
      result = Resource.create();
      ~~allocated.add(result);~~
    } else {
        result = available.pop();
        ~~allocated.add(result);~~
    }

    **allocated.add(result);**

    return result;
  }

  private Deque<Resource> available;
  private List<Resource> allocated;

}

3항 연산자로 바꾼다

public class ResourcePool {
  public Resource get() {
    ~~Resource result;

    if (available.isEmpty()) {
      result = Resource.create();
    } else {
        result = available.pop();
    }~~
        Resource result = available.isEmpty() ? Resource.create() : available.pop();
    allocated.add(result);

    return result;
  }

  private Deque<Resource> available;
  private List<Resource> allocated;

}
728x90

'Study > Refactoring' 카테고리의 다른 글

리팩터링 1  (0) 2023.08.31
리팩터링 12  (0) 2023.08.30
리팩터링 챕터 10  (0) 2023.08.28
리팩터링 챕터 9  (0) 2023.08.27
리팩토링 챕터 8  (0) 2023.07.25