본문 바로가기

리팩토링(마틴 파울러)

리팩토링 : 1장 첫 번째 예제 - 메서드의 분해 및 재분배

1장의 전체적인 내용 : 1장에서는 흔한 디자인 상의 결점을 갖고 있는 작은 프로그램을 갖고 리팩토링을 해서 만족할 만한 수준의 객체 지향 프로그램으로 만드는 과정을 그린다. 우리는 이 과정에서 리팩토링의 프로세스와 몇가지 유용한 리팩토링을 적용하는 것을 본다. 이를 통해 리팩토링이란 무엇인가를 어느정도 이해할 수 있게 한다.

 

예제 - 비디오 가게에서 고객이 어떤 영화를 얼마나 오랫동안 빌렸는 지 보여주고 이 영화의 종류와 대여기간에 따라 요금을 계산하는 프로그램

 

영화의 종류는 보통, 어린이용, 최신 이렇게 세 가지 종류로 나뉜다.

 

  • Movie

package com.heejin.ex01;

public class Movie {
  public static final int CHILDREN = 2;
  public static final int REGULAR = 0;
  public static final int NEW_RELEASE = 1;
  
  private String _title;
  private int _priceCode;
  
  public Movie(String title, int priceCode) {
    _title = title;
    _priceCode = priceCode;
  }
  
  public int getPriceCode() {
    return _priceCode;
  }
  
  public void setPriceCode(int arg) {
    _priceCode = arg;
  }
  
  public String getTitle() {
    return _title;
  }
}
  • Rental

package com.heejin.ex01;

public class Rental {
  private Movie _movie;
  private int _daysRented;
  
  public Rental(Movie movie, int daysRented) {
    _movie = movie;
    _daysRented = daysRented;
  }
  
  public int getDaysRented() {
    return _daysRented;
  }
  
  public Movie getMovie() {
    return _movie;
  }

}
  • Customer

package com.heejin.ex01;

import java.util.Enumeration;
import java.util.Vector;

public class Customer {
  private String _name;
  private Vector _rentals = new Vector();
 
  public Customer(String name) {
    _name = name;
  }
  
  public void addRental(Rental arg) {
    _rentals.addElement(arg);
  }
  
  public String getName() {
    return _name;
  }
  
  public String statement() {
    double totalAmount = 0;
    int frequentRenterPoints = 0;
    Enumeration rentals = this._rentals.elements();
    String result = "Rental Record for " + this.getName() + "\n";
    
    while (rentals.hasMoreElements()) {
      double thisAmount = 0;
      Rental each = (Rental)rentals.nextElement();
      
      // 각 영화에 대한 요금 결정
      switch (each.getMovie().getPriceCode()) {
        case Movie.REGULAR:
          thisAmount += 2;
          if (each.getDaysRented() > 2) {
            thisAmount += (each.getDaysRented() - 2) * 1.5;
          }
          break;
        case Movie.NEW_RELEASE:
          thisAmount += each.getDaysRented() * 3;
          break;
        case Movie.CHILDREN:
          thisAmount += 1.5;
          if (each.getDaysRented() > 3) {
            thisAmount += (each.getDaysRented() - 3) * 1.5;
          }
          break;
      }
      
      // 포인트 추가
      frequentRenterPoints++;
      // 최신을 이틀 이상 대여하는 경우 추가 포인트 제공
      if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
          (each.getDaysRented() > 1)) {
        frequentRenterPoints++;
      }
      
      // 이 대여에 대한 요금 계산 결과 표시
      result += "\t" + each.getMovie().getTitle() + "\t" +
      String.valueOf(thisAmount) + "\n";
      totalAmount += thisAmount;
    }
    
    // 풋터(footer) 추가
    result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
    result += "You earned " + String.valueOf(frequentRenterPoints) +
        "frequent renter points";
    return result;
    
  }
}

 * Vector 

Vector 는 ArrayList와 동일한 내부구조를 갖는다. ArrayList와 마찬가지로 Vector 내부에 값이 추가되면 자동으로 크기가 조절되고 객체들은 한자리씩 이동한다. 한가지 다른점은 Vector은 동기화되어있고 ArrayList 가 비동기화되어있다는 사실이다.
 * Vector의 단점 
스레드가 한개일때도 무조건 동기화를 하므로 ArrayList보다 성능이 떨어진다. ArrayList의 기본적인 기능은 동일하지만 자동 동기화가 아닌 동기화 옵션이 존재한다.

 

 * Enumeration 

Iterator에서 remove() 메서드만 없고 거의 동일한 역할을 하는 인터페이스이다. Iterator가 나오기 이전 버전이므로 요즘은 가능한한 Iterator를 사용하고 있다.

Enumeration와 Iterator 는 둘다 인터페이스 이므로 new 연산자를 통해 생성하지 못한다. 따라서 다른 클래스에서 제공하는 메서드를 통해서 인터페이스를 구현하는 클래스를 생성해야한다.
 - Iterator는 (컬렉션 객체).iterator();를 통해,
 - Enumeration은 (컬렉션 객체).elements();를 통해 구현된다.

 

 * Enumeration 의 메서드 
 - Enumeration에서 객체를 꺼내는 메서드는 nextElement()이다.
 - Enumeration에서 꺼낼 수 있는 객체가 있는지 확인하는 메서드는 hasMoreElements()이다.

 

간단한 코드를 통해 이 프로그램을 테스트 해볼 수 있다.

package com.heejin.ex01;

public class App {
  public static void main(String[] args) {
    Customer c1 = new Customer("희진");
    c1.addRental(new Rental(new Movie("바람과 함께 사라지다", Movie.CHILDREN), 1));
    c1.addRental(new Rental(new Movie("벤자민 버튼의 시간은 거꾸로 간다", Movie.CHILDREN),4));
    System.out.println(c1.statement());
    System.out.println("-------------");
    
    Customer c2 = new Customer("희주");
    c2.addRental(new Rental(new Movie("버즈 오프 프레이", Movie.REGULAR), 1));
    c2.addRental(new Rental(new Movie("어스", Movie.REGULAR),5));
    System.out.println(c2.statement());
    System.out.println("-------------");
    
    Customer c3 = new Customer("호준");
    c3.addRental(new Rental(new Movie("미세스 다웃파이어", Movie.NEW_RELEASE), 2));
    c3.addRental(new Rental(new Movie("마틸다", Movie.NEW_RELEASE), 6));
    System.out.println(c3.statement());
    System.out.println("-------------");
    
  }

}

결과
Rental Record for 희진
	바람과 함께 사라지다	1.5
	벤자민 버튼의 시간은 거꾸로 간다	3.0
Amount owed is 4.5
You earned 2frequent renter points
-------------
Rental Record for 희주
	버즈 오프 프레이	2.0
	어스	6.5
Amount owed is 8.5
You earned 2frequent renter points
-------------
Rental Record for 호준
	미세스 다웃파이어	6.0
	마틸다	18.0
Amount owed is 24.0
You earned 4frequent renter points
-------------


 이 코드가 안 좋은 코드인 이유 

 

1. Customer의 클래스 루틴이 너무 길다. 이 루틴에 있는 많은 부분들이 다른 클래스에서 처리되어야한다.

2. 변경 사항에 따른 코드 수정이 복잡해지고 버그 위험도 커진다.

-> 계산서가 HTML로 웹페이지에 출력되는 프로그램으로 수정한다면, 계산서를 HTML로 인쇄하기 위해 재사용될 수 있는 부분이 없다.

     따라서 많은 부분이 중복된 완전히 새로운 메서드를 만들어야한다. 또한 이렇게 메서드를 새로 만든다고 해도, 메서드 안에서의 요금             계산 방법이 달라지면 모든 중복된 메서드들의 코드를 수정해야한다. 영화의 분류법을 변경한다고 해도 마찬가지이다.

     수정되어야할 코드가 늘어나면 그만큼 버그의 가능성도 늘어난다.

 

 테스트 구현 

 

리팩토링 할 부분의 코드에 대한 견고한 테스트 세트(test set)를 만든다. 리팩토링 과정은 사람이 하는 일이기에 언제든지 실수가 있을 수 있으며 이 과정에서 발생하는 모든 버그를 잡을 수 있을 만큼 견고한 테스트가 필요하다. 이런 테스트를 자체 검사(self-checking)이라고 부른다. 

 

 statement 메서드의 분해 및 재분배 

Refactoring/src/main/java com.heejin.ex01_1

메서드 추출

가장 먼저 해야하는 일은 지나치게 긴 statement 메서드를 조각으로 분해하는 것이다. 작은 조각의 코드는 관리하기 쉽기 때문이다.

이를 통해 우리의 목표는 중복을 최소화하면서 HTML statement를 만들기 쉽도록 하는 것이다.

 

하나의 메서드에서 다른 메서드를 추출하는 과정은 버그를 야기할 위험성을 갖는다. 따라서 어떻게 하면 안전하게 작업이 가능한 지 생각해야한다. 가장 먼저 메서드 내의 지역변수와 파라미터를 고려한다.

statement의 지역 변수로는 each 와 thisAmount가 있다.
each는 값이 수정되지 않는 변수이고, thisAmount그때그때 값이 달라질 수 있는 변수이다.

  • 수정되지 않는 변수는 추출할 메서드의 파라미터로 주면 좋다.
  • 값이 바뀌는 변수는 신중히 다뤄야하는 데 변수가 하나라면 리턴값으로 주는 것이 좋다. 
package com.heejin.ex01;

import java.util.Enumeration;
import java.util.Vector;

public class Customer {
  private String _name;
  private Vector _rentals = new Vector();

  public Customer(String name) {
    _name = name;
  }
  
  public void addRental(Rental arg) {
    _rentals.addElement(arg);
  }
  
  public String getName() {
    return _name;
  }
  
  public String statement() {
    double totalAmount = 0;
    int frequentRenterPoints = 0;
    Enumeration rentals = this._rentals.elements();
    String result = "Rental Record for " + this.getName() + "\n";
    while (rentals.hasMoreElements()) {
      double thisAmount = 0;
      Rental each = (Rental)rentals.nextElement();
      
      // 각 영화에 대한 요금 결정
      thisAmount = amountFor(each);
      
      // 포인트 추가
      frequentRenterPoints++;
      // 최신을 이틀 이상 대여하는 경우 추가 포인트 제공
      if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
          (each.getDaysRented() > 1)) {
        frequentRenterPoints++;
      }
      
      // 이 대여에 대한 요금 계산 결과 표시
      result += "\t" + each.getMovie().getTitle() + "\t" +
      String.valueOf(thisAmount) + "\n";
      totalAmount += thisAmount;
    }
    
    // 풋터(footer) 추가
    result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
    result += "You earned " + String.valueOf(frequentRenterPoints) +
        "frequent renter points";
    return result;
    
  }
  
  private int amountFor(Rental each) {
    int thisAmount = 0;
    switch (each.getMovie().getPriceCode()) {
      case Movie.REGULAR:
        thisAmount += 2;
        if (each.getDaysRented() > 2) {
          thisAmount += (each.getDaysRented() - 2) * 1.5;
        }
        break;
      case Movie.NEW_RELEASE:
        thisAmount += each.getDaysRented() * 3;
        break;
      case Movie.CHILDREN:
        thisAmount += 1.5;
        if (each.getDaysRented() > 3) {
          thisAmount += (each.getDaysRented() - 3) * 1.5;
        }
        break;
    }
    return thisAmount;
  }
}

아쉽게도 해당 코드에는 버그가 있다. 

Rental Record for 희진
	바람과 함께 사라지다	1.0
	벤자민 버튼의 시간은 거꾸로 간다	2.0
Amount owed is 3.0
You earned 2frequent renter points
-------------
Rental Record for 희주
	버즈 오프 프레이	2.0
	어스	6.0
Amount owed is 8.0
You earned 2frequent renter points
-------------
Rental Record for 호준
	미세스 다웃파이어	6.0
	마틸다	18.0
Amount owed is 24.0
You earned 4frequent renter points
-------------

테스트 결과를 보면 알 수 있지만, 소수점 아래 수들이 모두 0이 되었다. 이는 amountFor의 리턴값의 데이터 타입을 double이 아닌 int로 지정했기 때문이다. 따라서 메서드의 리턴값과 지역 변수 thisAmount의 데이터 타입을 double로 수정해준다.

package com.heejin.ex01;

import java.util.Enumeration;
import java.util.Vector;

public class Customer {
  private String _name;
  private Vector _rentals = new Vector();
  
  public Customer(String name) {
    _name = name;
  }
  
  public void addRental(Rental arg) {
    _rentals.addElement(arg);
  }
  
  public String getName() {
    return _name;
  }
  
  public String statement() {
    double totalAmount = 0;
    int frequentRenterPoints = 0;
    Enumeration rentals = this._rentals.elements();
    String result = "Rental Record for " + this.getName() + "\n";
    while (rentals.hasMoreElements()) {
      double thisAmount = 0;
      Rental each = (Rental)rentals.nextElement();
      
      // 각 영화에 대한 요금 결정
      thisAmount = amountFor(each);
      
      // 포인트 추가
      frequentRenterPoints++;
      // 최신을 이틀 이상 대여하는 경우 추가 포인트 제공
      if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
          (each.getDaysRented() > 1)) {
        frequentRenterPoints++;
      }
      
      // 이 대여에 대한 요금 계산 결과 표시
      result += "\t" + each.getMovie().getTitle() + "\t" +
      String.valueOf(thisAmount) + "\n";
      totalAmount += thisAmount;
    }
    
    // 풋터(footer) 추가
    result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
    result += "You earned " + String.valueOf(frequentRenterPoints) +
        "frequent renter points";
    return result;
    
  }
  
  private double amountFor(Rental each) {
    double thisAmount = 0;
    switch (each.getMovie().getPriceCode()) {
      case Movie.REGULAR:
        thisAmount += 2;
        if (each.getDaysRented() > 2) {
          thisAmount += (each.getDaysRented() - 2) * 1.5;
        }
        break;
      case Movie.NEW_RELEASE:
        thisAmount += each.getDaysRented() * 3;
        break;
      case Movie.CHILDREN:
        thisAmount += 1.5;
        if (each.getDaysRented() > 3) {
          thisAmount += (each.getDaysRented() - 3) * 1.5;
        }
        break;
    }
    return thisAmount;
  }
}

*이런 간단한 형태의 리팩토링 단계는 다양한 통합개발 환경에서 extract method와 같이 지원하고 있는 기능을 사용할 수도 있다.  

 

메서드를 추출했으면 그에 맞게 변수명도 바꿔준다.

  • thisAmount => result
  • each => aRental
  private double amountFor(Rental aRental) {
    double result = 0;
    switch (aRental.getMovie().getPriceCode()) {
      case Movie.REGULAR:
        result += 2;
        if (aRental.getDaysRented() > 2) {
          result += (aRental.getDaysRented() - 2) * 1.5;
        }
        break;
      case Movie.NEW_RELEASE:
        result += aRental.getDaysRented() * 3;
        break;
      case Movie.CHILDREN:
        result += 1.5;
        if (aRental.getDaysRented() > 3) {
          result += (aRental.getDaysRented() - 3) * 1.5;
        }
        break;
    }
    return result;
  }

변수의 이름을 적절하게 바꾸는 일은 해당 코드가 무엇을 하고 있는 지 명확하게 나타나기 위함이며, 적절한 변수 이름은 명확한 코드를 만드는 핵심 중 하나이다. 코드가 그 목적을 잘 전달하는 것은 매우 중요하다. 

 

amount 계산 옮기기

amountFor 메서드를 유심히 보면, Customer 클래스 내의 정보를 사용하지 않고, Rental 클래스 내의 정보만을 사용하고 있다. 따라서 메서드의 위치가 Customer가 아닌 Rental 클래스에 있어야함을 의심해볼 수 있다. 대부분의 경우 메서드는 그것이 사용되는 데이터가 있는 객체에 있어야한다. 따라서 이 메서드는 Rental 클래스로 옮겨져야한다. 코드를 복사해서 옮긴 후 클래스에 맞게 코드를 수정한다.

  • private 접근제어자 삭제
  • 메서드 명 amountFor() => getCharge()
  • 파라미터 삭제
  • Custormer 클래스의 statement() 메서드에서 호출된 메서드 명 수정

Rental 클래스의 getCharge() 메서드

double getCharge() {
    double result = 0;
    switch (this.getMovie().getPriceCode()) {
      case Movie.REGULAR:
        result += 2;
        if (this.getDaysRented() > 2) {
          result += (this.getDaysRented() - 2) * 1.5;
        }
        break;
      case Movie.NEW_RELEASE:
        result += this.getDaysRented() * 3;
        break;
      case Movie.CHILDREN:
        result += 1.5;
        if (this.getDaysRented() > 3) {
          result += (this.getDaysRented() - 3) * 1.5;
        }
        break;
    }
    return result;
  }

Customer 클래스의 statement() 메서드에서 getCharge() 호출 부분

thisAmount = each.getCharge();

 

메서드가 추출되고 난 이후로부터는 thisAmount의 쓸모가 불분명해진다. 따라서 이를 삭제해주는 것이 좋다. 이와 같이 잠시 사용되는 임시 변수는 사용을 최대한 줄여야한다. 임시변수는 종종 쓸데 없는 파라미터를 양산하고, 임시 변수의 목적이 무엇인지 잊기도 쉽기 떄문이다.

  public String statement() {
    double totalAmount = 0;
    int frequentRenterPoints = 0;
    Enumeration rentals = this._rentals.elements();
    String result = "Rental Record for " + this.getName() + "\n";
    while (rentals.hasMoreElements()) {
      Rental each = (Rental)rentals.nextElement();
      
      // 포인트 추가
      frequentRenterPoints++;
      
      // 최신을 이틀 이상 대여하는 경우 추가 포인트 제공
      if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
          (each.getDaysRented() > 1)) {
        frequentRenterPoints++;
      }
      
      // 이 대여에 대한 요금 계산 결과 표시
      result += "\t" + each.getMovie().getTitle() + "\t" +
      String.valueOf(each.getCharge()) + "\n";
      
      // 각 영화에 대한 요금 결정
      totalAmount += each.getCharge();
    }
    
    // 풋터(footer) 추가
    result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
    result += "You earned " + String.valueOf(frequentRenterPoints) +
        "frequent renter points";
    return result;
    
  }

이와 같은 코드에서는  물론 getCharge()가 두번 호출되기 때문에 퍼포먼스 측면에서는 손해이다. 그러나 이것은 Rental 클래스에서 최적화될 수 있고, 코드가 적절히 분해되어있다면 더욱 효과적으로 최적화될 수 있다. 

 

포인트(frequent renter points) 계산 부분 추출

요금을 계산하는 부분을 메서드로 추출했던 것처럼 포인트 계산 부분도 영화 종류에 따라 달라지기 때문에 이 계산 부분을 별도의 메서드로 호출하는 것이 좋아보인다. 또한 이것도 Rental 클래스 쪽으로 옮겨주는 것이 바람직하다. 

 

이 과정에서 또한 중요하게 봐야할 부분은 추출한 부분이 사용하는 변수이다. 

  • each는 저번과 같이 파라미터로 주거나 Rental로 옮겨서 생략할 수 있다.
  • FrequentRengerPoints는 그 안의 값을 읽지 않기 때문에 굳이 변수로 갖고 올 필요가 없다.

Customer 클래스의 statement() 메서드 부분

public String statement() {
    double totalAmount = 0;
    int frequentRenterPoints = 0;
    Enumeration rentals = this._rentals.elements();
    String result = "Rental Record for " + this.getName() + "\n";
    while (rentals.hasMoreElements()) {
      Rental each = (Rental)rentals.nextElement();
      
      // 포인트 계산
      frequentRenterPoints += each.getFrequentRenterPoints();
      
      // 이 대여에 대한 요금 계산 결과 표시
      result += "\t" + each.getMovie().getTitle() + "\t" +
      String.valueOf(each.getCharge()) + "\n";
      
      // 각 영화에 대한 요금 결정
      totalAmount += each.getCharge();
    }

Rental 클래스의 getFrequentRenterPoints() 메서드 부분

int getFrequentRenterPoints() {
    // 최신을 이틀 이상 대여하는 경우 추가 포인트 제공
    if ((this.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
        (this.getDaysRented() > 1)) {
      return 2;
    }
    return 1;
  }

 

임시변수 제거하기

Customer의 statement() 메서드에서는 최종적으로 두 개의 임시 변수를 사용하고 있다. totalAmount와 frequentRenterPoints이다. 이 임시 변수는 고객이 대여한 것에 대한 총계를 얻는데 사용된다. 이 총계는 ASCII와 HTML 버전에서 모두 필요한 값들이기 때문에 질의 메서드(?)로 바꿔주는 것이 좋다.

  • totalAmount => getTotalCharge()
public String statement() {
    int frequentRenterPoints = 0;
    Enumeration rentals = this._rentals.elements();
    String result = "Rental Record for " + this.getName() + "\n";
    while (rentals.hasMoreElements()) {
      Rental each = (Rental)rentals.nextElement();
      
      frequentRenterPoints += each.getFrequentRenterPoints();
      
      // 이 대여에 대한 요금 계산 결과 표시
      result += "\t" + each.getMovie().getTitle() + "\t" +
      String.valueOf(each.getCharge()) + "\n";
      
    }
    
    // 풋터(footer) 추가
    result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
    result += "You earned " + String.valueOf(frequentRenterPoints) +
        "frequent renter points";
    return result;
    
  }
  private double getTotalCharge() {
    double result = 0;
    Enumeration rentals = this._rentals.elements();
    while (rentals.hasMoreElements()) {
      Rental each = (Rental) rentals.nextElement();
      result += each.getCharge();
    }
    return result;
  }
  • frequentRenterPoints => getTotalRenterPoints()
package com.heejin.ex01;

import java.util.Enumeration;
import java.util.Vector;

public class Customer {
  public Customer(String name) {
    _name = name;
  }
  
  public void addRental(Rental arg) {
    _rentals.addElement(arg);
  }
  
  public String getName() {
    return _name;
  }
  
  public String statement() {
    Enumeration rentals = this._rentals.elements();
    String result = "Rental Record for " + this.getName() + "\n";
    while (rentals.hasMoreElements()) {
      Rental each = (Rental)rentals.nextElement();
      
      // 이 대여에 대한 요금 계산 결과 표시
      result += "\t" + each.getMovie().getTitle() + "\t" +
      String.valueOf(each.getCharge()) + "\n";
      
    }
    
    // 풋터(footer) 추가
    result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
    result += "You earned " + String.valueOf(getTotalRenterPoints()) +
        "frequent renter points";
    return result;
    
  }
  
  private double getTotalCharge() {
    double result = 0;
    Enumeration rentals = this._rentals.elements();
    while (rentals.hasMoreElements()) {
      Rental each = (Rental) rentals.nextElement();
      result += each.getCharge();
    }
    return result;
  }
  
  private double getTotalRenterPoints() {
    double result = 0;
    Enumeration rentals = this._rentals.elements();
    while (rentals.hasMoreElements()) {
      Rental each = (Rental) rentals.nextElement();
      result += each.getFrequentRenterPoints();
    }
    return result;
  }
}

 

이로써 발생되는 문제??

  • 코드의 길이가 늘어났다 : 합계를 구하는 루프를 구성하는 데 많은 명령문을 필요로 하기 때문이다. 한 줄로 끝날 수 있는 간단한 합계 계산인데도 불구하고 루프를 구성하는데 여섯줄의 코드가 필요해진 것이다. 그러나 코드의 많은 부분이 같고, 어느 프로그래머에게나 명확한 형식이다.
  • 퍼포먼스 측면에서 효율성이 저하됐다 : 리팩토링 전의 코드는 while 문을 한번만 실행하는 데, 리팩토링 이후 while 문의 실행 횟수가 세번으로 늘어났다. 그러나 이 사실 만으로는 정말로 얼만큼이나 저하가 됐는 지는 프로파일링(profiling) 전까지는 알 수 없는 것이며, 실제로 저하됐다하더라도 리팩토링을 함으로써 최적화를 효과적으로 할 수 있는 기회를 더 많이 갖게 된다.

 결론 

 

두 개의 질의 메서드를 통해서 새롭게 추가되는 메서드나 기능들이 총계를 구하기 위해 Rental 클래스를 참조할 필요가 없어졌다. 어디에서나 이 두 질의 메서드가 사용가능해졌기 때문이다.


이제는 htmlStatement 메서드를 쉽게 추가할 수 있게 되었다. 

public String htmlStatement() {
    Enumeration rentals = this._rentals.elements();
    String result = "<H1>Rental Record for <EM>" + this.getName() + "</EM></H1>\n";
    while (rentals.hasMoreElements()) {
      Rental each = (Rental)rentals.nextElement();
      
      // 이 대여에 대한 요금 계산 결과 표시
      result += each.getMovie().getTitle() + ": " +
      String.valueOf(each.getCharge()) + "<BR>\n";
      
    }
    
    // 풋터(footer) 추가
    result += "<P>Amount owed is <EM>" + String.valueOf(getTotalCharge()) + "</EM><P>\n";
    result += "You earned <EM>" + String.valueOf(getTotalRenterPoints()) +
        "</EM> frequent renter points<P>";
    return result;
  }

statement 메서드에 있던 모든 계산 코드가 별도의 메서드로 추출됨으로써 재사용되었다. 복붙이 아니었기 때문에 계산 방법을 변경한다고 해도 그 계산에 해당하는 메서드 한 부분만 수정하면 된다. 일부 코드는 원래 statement 메서드에서 그대로 가져왔으나, 주로 루프를 구성하는 부분이다. 더 나아간 리팩토링을 통해서 이 문제도 깔끔히 해결 가능하다. 이런 작업은 Template Method의 예제를 참고할 수 있다.