맛보기 예제
Refactoring - 맛보기 예제
원래의 프로그램
맛보기 예제는 단순히 비디오 대여점에서 고객의 대여료 내역을 계산하고 출력하는 간단한 프로그램이다. 이 프로그램은 고객이 대여한 비디오와 대여기간을 표시한 후, 비디오 종류와 대여 기간을 토대로 대여료를 계산한다. 비디오 종류에는 일반물, 아동물, 최신물 세종류다. 대여료 계산과 더불어 내역을 바탕으로 적립 포인트도 계산되는데, 이 포인트는 비디오가 최신물인지 아닌지에 따라 달라진다.
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클래스로 옮긴다.
상태 패턴을 적용하는 작업은 이렇듯 상당히 복잡한데, 과연 이렇게까지 해서 적용할 가치가 있을까? 상태패턴을 적용하면 대여료 계산 방식을 변경하거나, 새 대여료를 추가하거나, 부수적인 대여료 관련 동작을 추가할 때 아주 쉽게 수정할 수 있다. 뿐만 아니라 프로그램의 다른 부분은 상태 패턴의 영향을 받지 않는다. 실제 큰 규모의 시스템에서는 무시할 수 없는 차이가 보인다.
정리
- 이장은 리팩토링이 무엇인지 어느 정도 감을 잡기 위한 장이다. 예제에서 몇가지 기법을 사용했는데. 이런 기법을 적용하면 기능 분배가 균등해지고 코드 유지보수도 쉬워진다. 가장 중요한 교훈은 ‘간단한 수정 -> 테스트’를 리듬처럼 반복해야 한다는 것이다. 이 리듬을 지킬 때만이 리팩토링을 빠르고 안정적으로 완료할 수 있다.
Share this post
Twitter
Google+
Facebook
Reddit
LinkedIn
StumbleUpon
Email