원래의 프로그램

  • 맛보기 예제는 단순히 비디오 대여점에서 고객의 대여료 내역을 계산하고 출력하는 간단한 프로그램이다. 이 프로그램은 고객이 대여한 비디오와 대여기간을 표시한 후, 비디오 종류와 대여 기간을 토대로 대여료를 계산한다. 비디오 종류에는 일반물, 아동물, 최신물 세종류다. 대여료 계산과 더불어 내역을 바탕으로 적립 포인트도 계산되는데, 이 포인트는 비디오가 최신물인지 아닌지에 따라 달라진다.

  • Movie클래스

public class Movie {
	public static final int CHILDRENS = 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 클래스 ```java

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 클래스
```java
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<Rental> rentals = _rentals.elements();
		String result = 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.CHILDRENS:
				thisAmount += 1.5;
				if(each.getDaysRented() > 3)
					thisAmount += (each.getDaysRented() - 3) * 1.5;
				break;	
			}
			
			//적립 포인트 1포인트 증
			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 += "누적 대여료 : " + String.valueOf(totalAmount) + "\n";
		result += "적립 포인트 : " + String.valueOf(frequentRenterPoints);
		return result;
	}
	
	
}
맛보기 프로그램설명
  • statement 메서드에서 지나치게 많은 기능이 들어 있는데 대부분의 기능은 다른 두 클래스에 들어가야 하는 기능이다.
  • 수정이 없을 경우면 상관이 없지만 수정이 필요한 경우 설계가 조잡하여 수정이 어렵다. 수정이 어려우면 버그가 생길 확률이 높다.
  • htmlStatement메서드를 추가해야한다. 그렇게 되면 기능을 수정할 때마다 두 메서드를 똑같이 수정해야 한다.
  • 위의 프로그램은 당장은 문제가 없지만 이러한 수정 사항이 생겼을 경우 수정하기가 힘들다. 그렇기 때문에 리팩토링을 해야할 시점이다.

리팩토링 첫 단계

  • 리팩토링의 첫단계는 신뢰도 높은 테스트를 작성하는 것이다. 아무리 체계적인 리팩토링 공식을 이용해 버그가 생길 수 있는 대부분의 원인을 방지하더라도, 인간인 이상 실 수할 수 있기 때문이다.

statement 메서드 분해와 기능 재분배

  • 긴 메서드를 분해해서 각 부분을 알맞은 클래스로 옮겨야 한다.
  • 이것은 중복 코드를 줄이고 htmlStatement를 간편하게 작성하기 위함이다.
우선 논리적 코드 뭉치를 찾아 메서드 추출 기법(Extract Method 142p)을 적용한다.
  • 여기서 확실히 분리할 부분은 switch문이다. amountFor메서드로 추출한다.
  • 추출후 바람직하지 않은 변수명을 수정한다. 변수명을 수정하면 무슨 기능을 하는지 분명히 드러낼 수 있기 때문에 아주 중요하다.
  • 변수명 수정이 끝났으니 컴파일과 테스트를 실시해서 에러가 없는지 확인한다. 리팩토링은 단계별로 테스트를 하면서 진행해야 한다.
대여료 계산 메서드 옮기기
  • amountFor메서드를 보면 Rental 클래스의 정보를 이용하고 정작 자신이 속한 Customer 클래스의 정보는 이용하지 않는다.
  • amountFor메서드가 잘못된 객체에 들어 있는 건 아닌지 의심을 해야한다. 메서드는 대체로 자신이 사용하는 데이터와 같은 객체에 들어 있어야 한다. 이 메서드는 Rental 클래스로 옮겨야 한다. 이 작업은 메서드 이동 (Move Method 178p) 기법을 실시하면 된다.
  • Rental 클래스에 getCharge란 메서드로 옮긴 후 테스트를 실행하고 문제가 없는지 확인한다.
  • thisAmount변수의 불필요한 중복. 따라서 임시변수를 메서드 호출로 전환(Replace Temp with Query 153p)기법을 사용해서 this 변수를 삭제한다. 임시변수가 많으면 불필요하게 많은 매개변수를 전달하게 되는 문제가 생긴다. 그리고 임시변수의 용도는 잊기 쉽다.
  • 기존 메서드 참조 부분을 전부 찾아서 새 메서드 참조로 수정해야 한다.
적립 포인트 계산을 메서드로 빼기
  • statement 메서드 안에서만 유효한 지역변수의 쓰임을 살펴보면 each가 사용되었는데 이것은 매개변수로 전달이 가능하다. 그리고 frequentRenterPoints가 임시변수로 사용되었는데 이미 값이 들어있다. 하지만 추출한 메서드안의 코드는 이 값을 읽을 수 없으나 추가로 대입문을 작성하면 임시변수를 매개변수로 전달할 필요는 없다.
  • getFrequentRenterPoints 메서드로 만들고 rental 클래스로 옮긴다.
임시변수 없애기
  • 임시변수는 문제가 생길 수 있다. 임시변수는 자체 루틴안에서만 효력이 있다 보니, 점점 더 ㅁ낳은 임시변수를 사용하게 되어 코드가 복잡해지기 쉽다. 현재 임시변수는 두 개 있으며, 두 변수는 해당 고객에 첨가된 대여료를 이용해 총 대여료를 계산할 때 사용된다. 총 대여로는 아스키 코드 내역과 HTML 내역 두 곳에 필요하다. 임시변수를 메서드 호출로 전환(Replace Temp with Query 153p)기법을 실시해서 totalAmount와 frequent RentalPoints 변수를 질의 메서드로 고치는 것을 선호한다.
private double getTotalCharge() {
		double result = 0;
		Iterator<Rental> rentals = _rentals.iterator();
		
		while (rentals.hasNext()) {
			Rental each = (Rental) rentals.next();
			result += each.getCharge();
		}
		return result;
	}

	private int getTotalFrequentRenterPoints() {
		int result = 0;
		
		Iterator<Rental> rentals = _rentals.iterator();
		
		while (rentals.hasNext()) {
			Rental each = (Rental) rentals.next();
			result += each.getFrequentRenterPoints();
		}
		return result;
	}
  • 대개 리팩토링은 코드 양이 줄게 마련인데 위의 리팩토링 기법은 코드양이 줄지 않고 늘었다. 이유는 java에서는 루프안에서 합산하는 데 많은 명령이 필요하기 때문이다.

  • 또하나의 문제점은 성능이다. 수정 전 코드는 while문 루프 1회만 실행되었는데 수정 후 코드는 3회나 실행된다. 오랜 시간이 걸리는 while 문으로 인해 성능이 저하된다. 많은 프로그래머들은 이런 이유 때문에 이 리팩토링을 하지 않으려 하지만, 항상 다양한 경우의 수를 생각하자. while문 리팩토링에 지레 겁먹을 필요는 없다. while 문은 최적화 단계에서 걱정해도 늦지 않다. 최적화 단계가 성능 해결의 적기이며 효과적인 최적화를 위한 더 많은 선택의 여지가 있다.

  • 이 메서드 호출들은 이제 Customer 클래스 안 어디서나 사용할 수 있다. 이제 시스템의 다른 부분에 이 정보가 필요하다면 이 메서드 호출들을 클래스의 public 인터페이스에 간단히 추가하면 된다. 이런 질의 메서드 호출 방식을 사용하지 않으면, 대여료 정보를 알아내고 루프안에서 계산하는 코드를 여러 다른 메서드에 넣어야 할 것이다. 복잡한 시스템에서는 그렇게 하면 작성할 코드도 많아지고 그에 따라 유지보수도 힘들어진다.

-htmlStatement 메서드를 추가하다

public String htmlStatement() {
		Iterator<Rental> rentals = _rentals.iterator();
		String result = "<H1><EM>" + getName() + "고객님의 대여 기록 </EM></H1><P>\n";
		while(rentals.hasNext()) {
			Rental each = (Rental) rentals.next();
			
			//모든 대여 비디오 정보와 대여로를 출
			result += each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "<BR>\n";
		}
		//footer
		result += "<P>누적 대여료 : <EM>" + String.valueOf(getTotalCharge()) + "</EM></P>\n";
		result += "<P>적립 포인트 : <EM>" + String.valueOf(getTotalFrequentRenterPoints()) + "</EM></P";
		return result;
	}
  • 계산 부분을 빼내서 htmlStatement 메서드로 작성하면 처음의 statement메서드에 들어있던 계산 코드를 전부 재사용할 수 있다. 복사해서 붙인 중복코드가 없으니 계산식 자체를 수정해야 할 때도 한 군데만 수정하면 된다.

가격 책정 부분의 조건문을 재정의로 교체

  • 새로운 요구사항이 생겼다. 대여점의 비디오 분류를 바꾸려고 준비 중이다. 어떻게 변경할지는 아직 결정하지는 않았지만, 분명한 건 기존과 전혀 다른 방식으로 분류하리란 것이다. 수정하는 각 비디오 분류마다 대여료와 적립 포인트의 적립 비율도 결정해야 한다. 지금 이런 식의 수정을 하기엔 무리다. 우선, 대여료 메서드와 적립 포인트 메서드부터 마무리 짓고 조건문 코드를 수정해서 비디오 분류를 변경해야 한다.

  • 제일 먼저 고칠 부분은 switch 문이다. 타 객체의 속성을 switch 문의 인자로 하는 것은 나쁜 방법이다. getCharge메서드를 Movie클래스로 옮긴다.

  • 대여기간을 Movie 클래스에 전달했는데 왜 그랬을까? 사용자가 요청한 변경이 단지 새로운 비디오 종류를 추가해 달라는 것이었기 때문이다. 비디오 종류를 변경해도 그로 인해 미치는 영향을 최소화하고자 대여료 계산을 Movie클래스 안에 넣은 것이다.

  • getFrequentRenterPoints메서드도 Movie클래스로 옮긴다.

마지막 단계 상속구조 만들기

-Movie 클래스는 비디오 종류에 따라 같은 메서드 호출에도 각기 다른 값을 반환한다. 그런데 이건 하위클래스가 처리할 일이다. 따라서 Movie 클래스를 상속받는 3개의 하위 클래스를 작성하고, 비디오 종류별 대여료 계산을 각 하위클래스에 넣어야 한다. (60~61p참조)

  • 인다이렉션(값 자체가 아니라 이름, 참조, 컨테이너 등을 사용해서 대상을 참조하는기능) 기능을 추가하면 Price 클래스 안의 코드를 하위 클래스로 만들어서 언제든 대여료를 변경할 수 있다.

-Price 클래스가 나타내는 것이 대여료 계산 알고리즘인가, 아니면 비디오의 상태인가?라는 의문이 든다. 현재는 Price 클래스의 코드는 비디오의 상태라고 생각한다.

  • 상태 패턴을 적용하려면 세 가지 리팩토링 기법을 사용해야 한다. 분류 부호를 상태/전략 패턴으로 전환(Replace Type Code with State/Strategy 273p)기법을 실시해서 분류 부호의 기능을 상태 패턴 안으로 옮겨야 한다. 그 다음에 메서드 이동(Move Method 178p)기법을 실시해서 switch 문을 Price 클래스 안으로 옮겨야 한다. 끝으로 조건문을 재정의로 전환(Replace Conditional with Polymorphism 305p) 기법을 실시해서 switch문을 없애야 한다.

  • 분류 부호를 상태/전략 패턴으로 전환(Replace Type Code with State/Strategy 273p)기법을 실시한다. 이 기법의 첫 단계는 분류 부호에 필드 자체 캡슐화 (Self Encapsulate Field 211p)기법을 적용해서 반드시 읽기/쓰기 메서드를 거쳐서만 분류 부호를 사용할 수 있게 해야한다.

  • _priceCode = priceCode; -> setPriceCode(priceCode)

  • 컴파일 후 문제가 없으면 Price 클래스를 상속 확장하는 클래스 3개를 추가로 작성하자.

public abstract class Price {
	abstract int getPriceCode();
}


class ChildrensPrice extends Price {

	@Override
	int getPriceCode() {
		return Movie.CHILDRENS;
	}
	
}

class NewReleasePrice extends Price {

	@Override
	int getPriceCode() {
		return Movie.NEW_RELEASE;
	}
	
}

class RegularPrice extends Price {

	@Override
	int getPriceCode() {
		return Movie.REGULAR;
	}
	
}
  • 이제 priceCode가 새 클래스를 사용할 수 있게 Movie클래스의 읽기/쓰기 메서드를 수정하자
        public int getPriceCode() {
		return _price.getPriceCode();
	}
	
	public void setPriceCode(int arg){
		switch(arg) {
			case REGULAR:
				_price = new RegularPrice();
				break;
			case CHILDRENS:
				_price = new ChildrensPrice();
				break;
			case NEW_RELEASE:
				_price = new NewReleasePrice();
				break;
		}
	
	}
  • 메서드 이동(Move Method 178p)기법을 실시해서 getCharge 메서드를 옮기자
  • 이후 조건문을 재정의로 전환(Replace Conditinal with Polymorphism 305p)기법을 실시한다. 이것은 switch문에 든 case문 코드를 가져다가 재정의 메서드로 작성하면 된다.
  • 이후 getFrequentRenterPoints 메서드를 Price클래스로 옮긴다.

  • 상태 패턴을 적용하는 작업은 이렇듯 상당히 복잡한데, 과연 이렇게까지 해서 적용할 가치가 있을까? 상태패턴을 적용하면 대여료 계산 방식을 변경하거나, 새 대여료를 추가하거나, 부수적인 대여료 관련 동작을 추가할 때 아주 쉽게 수정할 수 있다. 뿐만 아니라 프로그램의 다른 부분은 상태 패턴의 영향을 받지 않는다. 실제 큰 규모의 시스템에서는 무시할 수 없는 차이가 보인다.

정리

  • 이장은 리팩토링이 무엇인지 어느 정도 감을 잡기 위한 장이다. 예제에서 몇가지 기법을 사용했는데. 이런 기법을 적용하면 기능 분배가 균등해지고 코드 유지보수도 쉬워진다. 가장 중요한 교훈은 ‘간단한 수정 -> 테스트’를 리듬처럼 반복해야 한다는 것이다. 이 리듬을 지킬 때만이 리팩토링을 빠르고 안정적으로 완료할 수 있다.