메서드 호출 단순화

Updated:

메서드 호출 단순화

1. 메서드 변경

  • 메서드명을 봐도 기능을 알 수 없을 땐 메서드명을 직관적인 이름으로 바꾸자.

1.1 동기

  • 코드는 컴퓨터보다 인간이 알아보기 쉽게 작성해야 한다.
  • 인간이 알아보기 쉬우려면 코드에 사용된 모든 이름이 적절해야 한다.
  • 이름을 잘 짓는 기술이 진정으로 노련한 프로그래머가 되는 열쇠다.

1.2 동기

  • 메서드 시그니처가 상위클래스나 하위클래스에 구현되어 있는지 검사하자. 만약 구현되어 있다면 각 구현부를 대상으로 다음 단계들을 실시한다.
  • 새 이름으로 새 메서드를 선언하자. 코드의 원래 내용을 새 메서드로 복사하고 적절히 수정한다.
  • 새 메서드를 호출하게 원본 메서드의 내용을 수정한다.
  • 원본 메서드 참조 부분을 전부 찾아서 새 메서드를 참조하게 수정한다.
  • 원본 메서드를 삭제 한다.

1.3 예제

  • 메서드 명만 바꾸는거라 패스

2. 매개변수 추가

  • 메서드가 자신을 호출한 부분의 정보를 더 많이 알아야 할 땐 객체에 그 정보를 전달할 수 있는 매개변수를 추가하자.

2.1 동기

  • 매개변수 추가가 항상 좋은 것은 아니다. 충분히 다른 방법이 있을 수 있다.
  • 매개변수가 늘어나면 좋지 않다. 그렇다고 절대하지 말라는 것은 아니다.

2.2 방법

  • 메서드 시그너처가 상위클래스나 하위클래스에 구현되어 있는지 검사하자. 구현되어 있다면 이 과정을 모든 구현부마다 실시하자.
  • 추가한 매개변수를 전달받는 새 메서드를 선언하자. 원본 메서드의 내용을 새 메서드로 복사하자.
  • 새 메서드를 호출하게 원본 메서드의 내용을 수정하자.
  • 원본 메서드 참조 부분을 찾아서 바꾸자.
  • 원본 메서드를 삭제하자.

3. 매개변수 제거

  • 메서드가 어떤 매개변수를 더 이상 사용하지 않을 땐 그 매개변수를 삭제하자.

3.1 동기

  • 매개변수는 필요한 정보를 전달한다.
  • 껍데기 매개변수를 제거하자.

3.2 방법

  • 메서드 시그니처가 상위클래스나 하위클래스에 구현되어 있는지 검사한다. 사용한다면 하지 말자.
  • 제저할 매개변수가 없는 새 메서드를 선언하자.
  • 원본 메서드의 내용을 새 메서드로 복사하여 수정하고 참조한 부분들 수정한다.
  • 원본 메서드 삭제하자.

4. 상태 변경 메서드와 값 반환 메서드를 분리

  • 값 반환 기능과 객체 상태 변경 기능이 한 메서드에 들어 있을 댄 질의 메서드와 변경 메서드로 분리하자.

4.1 동기

  • 값을 반환하는 모든 메서드는 눈에 띄는 부작용이 없어야 한다.
  • 값을 반환하는 메서드가 있는데 그 메서드에 부작용이 있다면 상태 변경 부분과 값 반환 부분을 별도의 메서드로 각각 분리해야 한다.

4.2 방법

  • 원본 메서드와 같은 값을 반환하는 값 반환 메서드를 작성하자.
    • 원본 메서드를 관찰하여 무엇을 반환하는지 알아내자. 반환된 값이 임시적이면 임시 할당 위치를 살펴보자.
  • 메서드 호출의 결과를 반환하게 원본 메서드를 수정하자.
    • 원본 메서드 안의 모든 return문은 다른 것을 반환하게 작성하지 말고 return newQuery()라고 작성해야 한다.
  • 각 호출에 대해 한 번의 원본 메서드 호출을 값 반환 메서드 호출로 수정하자. 값 반환 메서드 호출 행 앞에 원본 메서드 호출을 추가하자.
  • 원본 메서드를 void 타입으로 수정하고 안의 return문을 삭제하자.

4.3 예제

  • 보안 시스템의 침입자 이름을 알려주고 경고 메시지를 보내는 함수는 다음과 같다.
  • 이 함수의 규칙은 침입자가 둘 이상일 때도 경고가 한 번만 송신되어야 한다는 점이다.
String foundMiscreant(String[] people) {
    for(int i = 0; i < people.length; i++) {
        if(people[i].equals("Don")) {
            sendAlert();
            return "Don";
        }

        if(people[i].equals("John")) {
            sendAlert();
            return "John";
        }
    }

    return "";
}

void checkSecurity(String[] people) {
    String found = foundMiscreant(people);
    someLaterCode(found);
}
  • 값 반환 코드를 상태 변경 코드와 분리하려면 우선 변경 메서드와 같은 값을 반환하되 부작용이 없는 적절한 질의 메서드를 다음과 같이 작성해야 한다.
String foundPerson(String[] people) {
    for(int i = 0; i < people.length; i++) {
        if(people[i].equals("Don")) {
            sendAlert();
            return "Don";
        }

        if(people[i].equals("John")) {
            sendAlert();
            return "John";
        }
    }

    return "";
}
  • 그런 다음, 원본 함수의 모든 return 문을 새 질의 호출로 한 번에 하나씩 수정하자. 각각을 수정할 때마다 테스트하자. 원본 메서드 수정을 모두 완료하면 다음과 같아진다.
String foundMiscreant(String[] people) {
    for(int i = 0; i < people.length; i++) {
        if(people[i].equals("Don")) {
            sendAlert();
            return foundPerson(people);
        }

        if(people[i].equals("John")) {
            sendAlert();
            return foundPerson(people);
        }
    }

    return foundPerson(people);
}
  • 이제 호출하는 메서드를 전부 수정해서 변경 메서드를 먼저 호출한 후 값 반환 메서드를 호출하게 만들자.
void checkSecurity(String[] people) {
    foundMiscreant(people);
    String found = foundPerson(people);
    someLaterCode(found);
}
  • 모든 호출 메서드를 수정했으면 다음과 같이 void 타입의 값을 반환하게 상태 변경 메서드를 수정하자.
void foundMiscreant(String[] people) {
    for(int i = 0; i < people.length; i++) {
        if(people[i].equals("Don")) {
            sendAlert();
            return;
        }

        if(people[i].equals("John")) {
            sendAlert();
            return;
        }
    }
}
  • 이제 원본 메서드명을 수정하자.
void sendAlert(String[] people) {
    for(int i = 0; i < people.length; i++) {
        if(people[i].equals("Don")) {
            sendAlert();
            return;
        }

        if(people[i].equals("John")) {
            sendAlert();
            return;
        }
    }
}
  • 앞의 코드에서 상태 변경 메서드가 값 반환 메서드의 코드를 이용하므로 코드가 중복되는 부분이 많다.
  • 따라서 다음과 같이 상태 변경 메서드에 알고리즘 전환을 적용하여 이러한 중복 코드를 다음과 같이 수정하면 된다.
void sendAlert(String[] people) {
    if(! foundPerson(people).equals("")) {
        sendAlert();
    }
}

5. 메서드를 매개변수로 전환

  • 여러 메서드가 기능은 비슷하고 안에 든 값만 다를 땐 서로 다른 값을 하나의 매개변수로 전달받는 메서드를 하나 작성하자.

5.1 동기

  • 기능은 비슷하지만 몇 가지 값에 따라 결과가 달라지는 메서드가 여러 개 있을 때 각 메서드를 전달된 매개변수에 따라 다른 작업을 처리하는 하나의 메서드로 만들면 편리하다.
  • 중복코드가 없어지고 매개변수 추가를 통해 다양한 것을 처리할 수 있어서 유연성도 커진다.

5.2 방법

  • 여러 메서드를 대체할 수 있는 매개변수 메서드를 작성하자.
  • 새 메서드를 호출하도록 원본 메서드 하나를 수정하자.

5.3 예제

5.3.1 예제 1
void tenPercentRaise() {
    salary *= 1.1;
}

void fivePercentRaise() {
    salary *= 1.05;
}
void raise(double factor) {
    salary *= (1 + factor);
}
5.3.2 예제 2
Dollars baseCharge() {
    double result = Math.min(lastUsage(), 100) * 0.03;

    if(lastUsage() > 100) {
        result += (Math.min(lastUsage(), 200) - 100) * 0.05;
    }

    if(lastUsage() > 200) {
        result += (lastUsage() - 200) * 0.07;
    }

    return new Dollars(result);
}
Dollars baseCharge() {
    double result = usageInRange(0,100) * 0.03;		
    result += usageInRange(100,200) * 0.05;
    result += usageInRange(200,Integer.MAX_VALUE) * 0.07;

    return new Dollars(result);
}

double usageInRange(int start, int end) {
    if(lastUsage() > start) {
        return Math.min(lastUsage(), end) - start;
    }

    return 0;
}

6. 매개변수를 메서드로 전환

  • 매개변수로 전달된 값에 따라 메서드가 다른 코드를 실행할 땐 그 매개변수로 전달될 수 있는 모든 값에 대응하는 메서드를 각각 작성하자.

6.1 동기

  • 한 매개변수의 값이 여러 개가 될 수 있을 때 조건문 안에서 각 값을 검사하여 다른 기능을 수행하는 메서드에 적용
  • 호출하는 부분은 매개변수에 값을 지정하여 무엇을 수행할지 판단해야 하므로, 여러 메서드를 작성하고 조건문은 없애는 것이 좋다.
  • 그러면 조건에 따른 실행도 방지하면서 컴파일할 때 검사가 된다는 장점이 있다.
  • 인터페이스도 더 명료해진다.
  • 그 메서드를 사용하는 프로그래머는 클래스에서 그 메서드가 사용된 부분을 관찰하고 유효한 매개변수 값도 알아내야 한다.
  • 매개변수 값이 많이 변할 가능성이 있을 때는 매개변수를 개별 메서드로 전환을 실시하면 안 된다.
  • 이럴 때 필드를 그냥 전달받은 매개변수로 지정하려면 간단한 속성 쓰기 메서드를 사용하면 된다.
  • 조건에 따라 다른 동작을 실행하게 해야 할 때는 조건문을 재정의로 전환을 실시해야 한다.

6.2 방법

  • 매개변수의 각 값에 해당하는 개별 메서드를 작성하자.
  • 조건문의 각 절마다 해당되는 새 메서드 호출을 넣자.
  • 각 절을 수정할 때마다 테스트를 실시하자.
  • 조건문이 든 원본 메서드의 각 호출 부분을 알맞은 새 메서드 호출로 바꾸자.
  • 호출 부분을 전부 고쳤으면 조건문이 든 매개변수 메서드를 삭제하자.

6.3 예제

public class Employee {
	static final int ENGINEER = 0;
	static final int SALESMAN = 1;
	static final int MANAGER = 2;
	
	static Employee create(int type) {
		switch(type) {
			case ENGINEER:
				return new Engineer();
			case SALESMAN:
				return new Salesman();
			case MANAGER:
				return new Manager();
			default:
				throw new IllegalArgumentException("없는 분류 부호 값");
		}
	}
}
  • 앞의 메서드는 팩토리 메서드라서 생성자를 조건문을 재정의로 전환을 적용할 수 없다.
  • 왜냐하면 객체를 아직 작성하지 않았기 때문이다.
  • 새 하위클래스가 별로 많지 않을 것 같으므로 명시적 인터페이스가 적절하다.
  • 다음과 같이 메서드 작성
static Employee createEngineer() {
    return new Engineer();
}

static Employee createSalesman() {
    return new Salesman();
}

static Employee createManager() {
    return new Manager();
}
  • case 문 안의 각 case를 새 개별 메서드 호출로 하나씩 바꾸자.
static Employee create(int type) {
    switch(type) {
        case ENGINEER:
            return Employee.createEngineer();
        case SALESMAN:
            return Employee.createSalesman();
        case MANAGER:
            return Employee.createManager();
        default:
            throw new IllegalArgumentException("없는 분류 부호 값");
    }
}
  • 이제 원본 create 메서드를 호출하는 부분을 작업하자.
Employee kent = Employee.create(ENGINEER);
Employee kent = Employee.createEngineer();
  • create메서드를 호출하는 부분을 모두 수정했으면 create 메서드 삭제하고 상수들 모두 삭제 하자.
public class Employee {
	static Employee createEngineer() {
		return new Engineer();
	}
	
	static Employee createSalesman() {
		return new Salesman();
	}
	
	static Employee createManager() {
		return new Manager();
	}
}

7. 객체를 통째로 전달

  • 객체에서 가져온 여러 값을 메서드 호출에서 매개변수로 전달할 땐 그 객체를 통째로 전달하게 수정하자.

7.1 동기

  • 객체가 한 객체에 든 여러 값을 메서드 호출할 때 매개변수로 전달하고 있다면 이 리팩토링 기법을 적용해야 한다.
  • 이럴 땐 호출된 객체가 나중에 새 데이터 값을 필요로 할 때마다 이 메서드를 호출하는 모든 부분을 찾아서 수정해야 한다는 문제가 있다.
  • 데이터를 넘겨주는 객체 자체를 넘기면 이 문제를 방지할 수 있다.
  • 객체를 통째로 전달을 실시하면 매개변수 세트 변경의 편의성뿐 아니라 코드를 알아보기도 쉬워진다.
  • 객체를 통째로 전달하는 방식의 단점은 값을 전달할 때 호출되는 객체가 그 값들에 의존하게 되지만 값이 추출된 객체에는 의존하지 않게 되는 점이다.
  • 통 객체를 전달하면 통 객체와 호출된 객체가 서로 의존하게 된다.
  • 의존성 구조를 망가뜨릴 것 같으면 객체를 통째로 전달을 실시하지 말아야 한다.

7.2 방법

  • 데이터가 속한 통 객체에 새 매개변수를 작성하자.
  • 통 객체에서 가져와야 할 매개변수를 파악하자.
  • 한 매개변수를 선택해서 메서드 안에서 그 매개변수를 참조하는 부분을 넘겨받은 통 객체 안의 적절한 메서드 호출로 바꾸자.
  • 매개변수를 삭제하자.
  • 통 객체에서 가져올 수 있는 모든 매개변수를 대상으로 위의 과정을 반복
  • 삭제한 매개변수들을 가져오는 호출 메섣드 안의 코드를 삭제한다.

7.3 예제

  • 하루 동안의 최고기온과 최저기온을 기록하는 Room 객체는 다음과 같다.
  • 이 온도 범위를 미리 정의한 난방 계획의 온도 범위와 비교해야 한다.
public class Room {
	boolean withinPlan(HeatingPlan plan) {
		int low = daysTempRange().getLow();
		int high = daysTempRange().getHight();
		return plan.withinRange(low, high);
	}
}

class HeatingPlan {
	private TempRange range;
	
	boolean withinRange(int low, int high) {
		return (low >= range.getLow() && high <= range.getHigh());
	}
}
  • 범위 정보를 일일이 전달할 것이 아니라 범위 객체를 통째로 전달하면 된다.
  • 이런 간단한 상황일 땐 통 객체를 전달하게 수정하는 작업이 단번에 끝난다.
  • 그러나 매개변수가 더 많이 필요할 때는 더 작은 단계들로 나눠서 해야 한다.
  • 우선 매개변수 나열 부분에 다음과 같이 통 객체를 추가하자.
public class Room {
	boolean withinPlan(HeatingPlan plan) {
		int low = daysTempRange().getLow();
		int high = daysTempRange().getHight();
		return plan.withinRange(daysTempRange(), low, high);
	}
}

class HeatingPlan {
	private TempRange range;
	
	boolean withinRange(TempRange roomRange, int low, int high) {
		return (low >= range.getLow() && high <= range.getHigh());
	}
}
  • 그런 다음, 매개 변수 중 하나를 택해서 그것을 통 객체에 있는 메서드로 교체하자.
  • 아래는 최종본
public class Room {
	boolean withinPlan(HeatingPlan plan) {
		return plan.withinRange(daysTempRange());
	}
}

class HeatingPlan {
	private TempRange range;
	
	boolean withinRange(TempRange roomRange) {
		return (roomRange.getLow() >= range.getLow() 
                && roomRange.getHigh() <= range.getHigh());
	}
}
  • 객체를 통째로 전달하는 방법에 익숙해지면, 나중에 통 객체로 기능을 옮겨야 작업하기 편하다.
public class Room {
	boolean withinPlan(HeatingPlan plan) {
		return plan.withinRange(daysTempRange());
	}
}

class HeatingPlan {
	private TempRange range;
	
	boolean withinRange(TempRange roomRange) {
		return range.includes(roomRange);
	}
}

class TempRange {
	boolean includes(TempRange arg) {
		return arg.getLow() >= this.getLow() && arg.getHigh() <= this.getHigh();
	}
}

8. 매개변수 세트를 메서드로 전환

  • 객체가 A 메서드를 호출해서 그 결과를 B 메서드에 매개변수로 전달하는데, 결과를 매개변수로 받는 B 메서드도 직접 A 메서드를 호출할 수 있을 땐 매개변수를 없애고 A 메서드를 B 메서드가 호출하게 하자.

8.1 동기

  • 메서드가 매개변수로 전달받는 값을 다른 방법으로도 가져올 수 있다면, 그 방법을 택해야 한다.
  • 매개변수 나열이 길면 코드가 복잡해지므로 가능하면 매개변수를 줄여야 한다.
  • 전달할 매개변수를 줄이려면 같은 계산을 수신 메서드도 할 수 있는지 검사해야 한다.
  • 객체가 자신의 메서드를 호출하지만 호출한 메서드의 매개변수가 계산에 전혀 사용되지 않는다면, 그 계산을 별도의 메서드로 만들고 매개변수를 삭제할 수 있다.
  • 호출하는 객체를 참조하는 다른 객체에 있는 메서드를 호출할 때도 마찬가지다.
  • 나중에 메서드를 매개변수 메서드로 바꿀 가능성에 대비해 매개변수를 두는 경우도 가끔 있는데 이럴 때는 삭제하자.

8.2 방법

  • 필요한다면 매개변수를 사용한 계산 부분을 별도의 메서드로 빼내자.
  • 메서드 안의 매개변수 사용 부분을 추출한 메서드 호출로 수정하자.
  • 매개변수를 대상으로 매개변수 제거를 실시하자.

8.3 예제

  • 할인 주문 에제를 현실성 없게 변형한 코드는 다음과 같다.
public class Test {
	private int quantity;
	private int itemPrice;

	public double getPrice() {
		int basePrice = quantity * itemPrice;
		int discountLevel;
		
		if(quantity > 100) {
			discountLevel = 2;
		} else {
			discountLevel = 1;
		}
		
		double finalPrice = discountedPrice(basePrice, discountLevel);
		
		return finalPrice;
	}

	private double discountedPrice(int basePrice, int discountLevel) {
		if(discountLevel == 2) {
			return basePrice * 0.1;
		}
		
		return basePrice * 0.05;
	}
}
  • 우선 할인 등급 계산 부분을 메서드로 추출하자.
public class Test {
	private int quantity;
	private int itemPrice;

	public double getPrice() {
		int basePrice = quantity * itemPrice;
		int discountLevel = getDiscountLevel();
		
		double finalPrice = discountedPrice(basePrice, discountLevel);
		
		return finalPrice;
	}

	private int getDiscountLevel() {
		if(quantity > 100) {
			return 2;
		} 
		
		return 1;
	}

	private double discountedPrice(int basePrice, int discountLevel) {
		if(discountLevel == 2) {
			return basePrice * 0.1;
		}
		
		return basePrice * 0.05;
	}
}
  • 그런 다음 discountedPrice 메서드 안의 매개변수 사용 부분을 전부 바꾼다.
public class Test {
	private int quantity;
	private int itemPrice;

	public double getPrice() {
		int basePrice = quantity * itemPrice;
		double finalPrice = discountedPrice(basePrice);
		
		return finalPrice;
	}

	private int getDiscountLevel() {
		if(quantity > 100) {
			return 2;
		} 
		
		return 1;
	}

	private double discountedPrice(int basePrice) {
		if(getDiscountLevel() == 2) {
			return basePrice * 0.1;
		}
		
		return basePrice * 0.05;
	}
}
  • 나머지 basePrice에 대해서도 변경
public class Test {
	private int quantity;
	private int itemPrice;
	
	private double getPrice() {
		if(getDiscountLevel() == 2) {
			return getBasePrice() * 0.1;
		}
		
		return getBasePrice() * 0.05;
	}

	private int getDiscountLevel() {
		if(quantity > 100) {
			return 2;
		} 
		
		return 1;
	}
	
	private int getBasePrice() {
		return quantity * itemPrice;
	}
}

9. 매개변수 세트를 객체로 전환

  • 여러 개의 매개변수가 항상 붙어 다닐 땐 그 매개변수들을 객체로 바꾸자.

9.1 동기

  • 특정 매개변수들이 늘 함께 전달되는 경우를 흔히 볼 수 있다.
  • 여러 메서드가 한 클래스나 여러 클래스에서 이 매개변수 집합을 사용할 가능성이 있다.
  • 이런 클래스들은 데이터 뭉치이므로 그 모든 데이터가 든 객체로 바꿀 수 있다.
  • 데이터를 그룹으로 묶으려면 이 매개변수들을 객체로 바꾸는 것이 좋다.
  • 새 객체에 정의된 속성 접근 메서드로 인해 코드의 일관성도 개선되고, 결과적으로 코드를 알아보거나 수정하기도 쉬워진다.
  • 더불어 매개변수를 한 덩이로 만들면 기능을 새 클래스로 옮길 수 있어서 훨씬 좋다.
  • 메서드 안에 매개변수 값에 대한 공통적인 조작을 넣는 경우가 많다.
  • 이 동작을 새 객체로 옮기면 상당량의 중복 코드를 없앨 수 있다.

9.2 방법

  • 대체할 매개변수 그룹에 해당하는 새 클래스를 작성하고, 그 클래스를 변경불가로 만들자.
  • 새 데이터 뭉치에 매개변수 추가를 적용하자. 새 매개변수에 기본 값을 사용하자.
    • 호출 부분이 많으면 기존 시그너처를 그대로 두고 새 메서드를 호출하게 하자. 그 리팩토링을 기존 메서드에 먼저 적용하자. 그런 다음 호출 부분들을 하나씩 옮긴 후 기존 메서드는 삭제하면 안된다.
  • 데이터 뭉치 안의 각 매개변수마다 시그너처에서 해당 매개변수를 삭제하자. 그 값 대신 매개변수 객체를 사용하게 호출 부분과 메서드 내용을 수정하자.
  • 매개변수 삭제를 전부 완료했으면, 메서드 이동을 적용하여 매개변수 객체로 옮길 수 있는 기능을 찾자.
    • 매개변수 객체로 옮길 수 있는 기능은 메서드 전체일 수도 있고 일부분일 수도 있다. 메서드의 일부분일 겨우에는 우선 메서드 추출을 실시한 후 새로 빼낸 메서드를 옮기면 된다.

9.3 예제

public class Entry {
	private double value;
	private Date chargeDate;
	
	Entry (double value, Date chargeDate) {
		this.value = value;
		this.chargeDate = chargeDate;
	}
	
	Date getDate() {
		return chargeDate;
	}
	
	double getValue() {
		return value;
	}
}
  • Account 클래스엔 입금액 컬렉션이 들어 있고, 두 날짜 사이의 계좌 입출금 현황을 알아내는 매서드가 들어 있다.
public class Account {
	private Vector<Entry> entries = new Vector<Entry>();
	
	double getFlowBetween(Date start, Date end) {
		double result = 0;
		Enumeration<Entry> e = entries.elements();
		
		while(e.hasMoreElements()) {
			Entry each = e.nextElement();
			
			if(each.getDate().equals(start) || each.getDate().equals(end)
					|| (each.getDate().after(start) && each.getDate().before(end))) {
				result += each.getValue();
			}
		}
		
		return result;
	}
}
/* 클라이언트 코드 */
double flow = anAccount.getFlowBetween(startDate, endDate);
  • 개시일과 폐쇄일, 큰 금액과 작은 금액 같이 범위를 나타내는 값 쌍이 얼마나 많은 부분에 들어 있을지 모른다.
  • 이럴 때 범위패턴을 사용한다.
  • 다음과 같이 범위를 처리하는 단순 데이터 클래스를 선언한다.
public class DateRange {
	private final Date start;
	private final Date end;
	
	DateRange (Date start, Date end) {
		this.start = start;
		this.end = end;
	}
	
	Date getStart() {
		return start;
	}
	
	Date getEnd() {
		return end;
	}
}
  • DateRange 클래스를 변경불가로 만든다.
  • 이러면 별칭 버그가 방지되어 좋다.
  • 자바의 매개변수는 값을 이용한 전달 방식을 사용하는데 클래스를 변경불가로 만들면 그러한 자바 매개변수의 원리를 흉내낼 수 있으므로, 이 리팩토링 기법을 적용하려면 반드시 DateRange 클래스를 변경불가로 만들어야 한다.

  • 다음으로 getFlowBetween 메서드의 매개변수 나열 부분에 다음과 같이 DateRange 클래스를 추가하자.
  • 그리고 매개변수 start, end를 삭제하고 호출 부분 변경하자.
public class Account {
	private Vector<Entry> entries = new Vector<Entry>();
	
	double getFlowBetween(DateRange range) {
		double result = 0;
		Enumeration<Entry> e = entries.elements();
		
		while(e.hasMoreElements()) {
			Entry each = e.nextElement();
			
			if(each.getDate().equals(range.getStart()) 
			|| each.getDate().equals(range.getEnd())
			|| (each.getDate().after(range.getStart()) 
				&& each.getDate().before(range.getEnd()))) {
				result += each.getValue();
			}
		}
		
		return result;
	}
}
/* 클라이언트 */
double flow = anAccount.getFlowBetween(new DateRange(startDate, endDate));
  • 이것으로 매개변수 나열 부분을 객체로 전환하는 작업은 마쳤지만, 기능을 새 객체의 다른 메서드로 옮기는 작업을 추가로 실시한다면 이 리팩토링의 결과가 더욱 빛날 것이다.
  • 메서드 추출, 메서드 이동을 적용하면 코드가 다음과 같아 진다.
public class Account {
	private Vector<Entry> entries = new Vector<Entry>();
	
	double getFlowBetween(DateRange range) {
		double result = 0;
		Enumeration<Entry> e = entries.elements();
		
		while(e.hasMoreElements()) {
			Entry each = e.nextElement();
			
			if(range.includes(each.getDate())) {
				result += each.getValue();
			}
		}
		
		return result;
	}
}

public class DateRange {
	...
	
	boolean includes(Date arg) {
		return arg.equals(start) || arg.equals(end) 
					|| (arg.after(start) && arg.before(end));
	}
}

10. 쓰기 메서드 제거

  • 생성할 때 지정한 필드 값이 절대로 변경되지 말아야 할 땐 그 필드를 설정하는 모든 쓰기 메서드를 삭제하자.

10.1 동기

  • 쓰기 메서드가 있다는 건 필드 값을 변경할 수 있다는 얘기다.
  • 객체가 생성된 후에는 필드가 변경되지 말아야 한다면, 쓰기 메서드를 작성하지 않아야 한다.
  • 그렇게 하면 확실히 의도가 달성되고 필드가 수정될 가능성을 차단할 수 있다.
  • 이 기법은 프로그래머가 간접적인 변수 접근을 맹목적으로 이용할 때 실시해야 한다.

10.2 방법

  • 쓰기 메서드가 생성할 때나 생성자가 호출하는 메서드에서만 호출되는지 검사하자.
  • 쓰기 메서드가 생성자 안이나 생성자가 호출한 메서드 안에서만 호출되는지 검사하자.
  • 변수에 직접 접근할 수 있게 생성자를 수정하자.
    • 상위클래스의 private 필드를 설정하는 하위클래스가 있으면 생성자를 변수에 직접 접근하게 수정할 수 없다. 이럴 땐 상위클래스에 private 필드 값을 설정하는 protected 메서드를 넣어야 한다. 상위클래스 메서드명은 쓰기 메서드와 혼동되지 않는 이름으로 정하자.
  • 쓰기 메서드를 삭제하자.

10.3 예제

public class Account {
	private String id;
	
	Account (String id) {
		setId(id);
	}
	
	void setId(String arg) {
		id = arg;
	}
}
  • 다음과 같이 바꾼다.
public class Account {
	private String id;
	
	Account (String id) {
		this.id = id;
	}
}
  • 문제는 코드를 조금 변형하면 드러난다.
  • 첫 번째 문제는 매개변수로 계산을 수행할 때 드러난다.
public class Account {
	private String id;
	
	Account (String id) {
		setId(id);
	}
	
	void setId(String arg) {
		id = "ZZ" + arg;
	}
}
  • 앞의 코드처럼 수정이 간단할 때는 생성자를 수정해도 되지만, 수정이 복잡하거나 별도의 메서드에서 호출해야 할 때는 따로 메서를 하나 작성해야 한다.
  • 이때 메서드 이름은 의도가 확실히 드러나게 정해야 한다.
public class Account {
	private String id;
	
	Account (String id) {
		initializeId(id);
	}
	
	void initializeId(String arg) {
		id = "ZZ" + arg;
	}
}
  • 이번에는 하위클래스가 상위클래스의 private 변수를 초기화하는 다음 까다로운 예제를 보자.
public class InterestAccount extends Account{
	private double interestRate;
	
	InterestAccount(String id, double rate) {
		setId(id);
		interestRate = rate;
	}
}
  • 문제는 id 변수에 직접 접근해서 값을 설정할 수 없다는 것이다.
  • 최선의 해결 방법은 상위클래스 생성자를 사용하는 것이다.
public class InterestAccount extends Account{
	private double interestRate;
	
	InterestAccount(String id, double rate) {
		super(id);
		interestRate = rate;
	}
}
  • 혹시 그 방법을 사용할 수 없을 땐, 다음과 같이 이름이 잘 지어진 메서드를 사용하는 게 최선이다.
public class InterestAccount extends Account{
	private double interestRate;
	
	InterestAccount(String id, double rate) {
		initializeId(id);
		interestRate = rate;
	}
}
  • 까다로운 또 한 가지 예는 다음과 같이 컬렉션 값을 설정하는 것이다.
public class Person {
	private Vector courses;
	
	Vector getCourses() {
		return courses;
	}
	
	void setCourses(Vector arg) {
		courses = arg;
	}
}
  • 쓰기메서드를 추가 및 삭제 기능으로 바꿔야 한다.
  • 그렇게 하는 방법은 컬렉션 캡슐화를 참고한다.

11. 메서드 은폐

  • 메서드가 다른 클래스에 사용되지 않을 땐 그 메서드의 반환 타입을 private로 만들자.

11.1 동기

  • 클래스 안에 넣은 기능이 많을수록 읽기/쓰기 메서드의 대부분은 더 이상 public으로 놔둘 이유가 없다.

11.2 방법

  • 주기적으로 개방도 낮출 수 있는지 확인하자.
  • 각 메서드를 가능하면 private 타입으로 만들자.

12. 생성자를 팩토리 메서드로 전환

  • 객체를 생성할 때 단순한 생성만 수행하게 해야 할 땐 생성자를 팩토리 메서드로 교체하자.

12.1 동기

  • 분류부호를 하위클래스로 바꿀 때 발생한다.
  • 분류부호를 사용해 작성한 객체가 있는데 현 시점에서 하위클래스가 필요해졌다.
  • 어느 하위클래스를 사용할지는 분류 부호에 따라 달라진다.
  • 하지만 생성자는 요청된 객체의 인스턴스 반환만 할 수 있다.
  • 따라서 생성자를 팩토리 메서드로 바꿔야 한다.
  • 생성자가 너무 제한되는 다른 상황에서도 팩토리 메서드를 사용할 수 있다.
  • 팩토리 메서드는 값을 참조로 전환을 실시하기 위해 꼭 필요하다.
  • 팩토리 메서드는 매개변수의 숫자와 타입을 벗어나는 다른 생성 동작을 나타낼 때도 사용할 수 있다.

12.2 방법

  • 팩토리 메서드를 작성하자. 그 메서드의 내용을 기존의 생성자 호출로 수정하자.
  • 모든 생성자 호출을 팩토리 메서드 호출로 바꾸자.
  • 생성자를 private으로 선언하자.

12.3 예제

public class Employee {
	private int type;
	static final int ENGINEER = 0;
	static final int SALESMAN = 1;
	static final int MANAGER = 2;
	
	Employee(int type) {
		this.type = type;
	}
}
  • 각 분류 부호에 해당하는 Employee 클래스의 하위클래스를 작성하려 한다.
  • 이를 위해선 팩토리 메서드를 다음과 같이 작성해야 한다.
public class Employee {
	private int type;
	static final int ENGINEER = 0;
	static final int SALESMAN = 1;
	static final int MANAGER = 2;
	
	Employee(int type) {
		this.type = type;
	}
	
	static Employee create(int type) {
		return new Employee(type);
	}
}
  • 다음으로, 생성자 호출 부분을 전부 새 메서드 호출로 고치고 생성자를 private 타입으로 바꾸자.
Employee eng = Employee.create(Employee.ENGINEER);
12.3.1 예제: 문자열을 사용하는 하위클래스 작성
  • 위에 개선된 점은 생성된 객체의 클래스에서 생성 호출의 결과를 받는 수신 메서드를 분리했다는 것이다.
  • 나중에 분류 부호를 하위클래스로 전환을 적용해서 분류 부호를 Employee의 하위클래스로 전환할 경우, 팩토리 메서드를 사용하면 이 하위클래스를 클라이언트가 볼 수 없게 은폐할 수 있다.
static Employee create(int type) {
    switch (type) {
        case ENGINEER:
            return new Engineer();
        case SALESMAN:
            return new Salesman();
        case MANAGER:
            return new Manager();
        default:
            throw new IllegalArgumentException("없는 분류 부호 값");
    }
}
  • 한가지 단점은 switch 문이 생긴다는 것이다. 새 하위클래스를 추가해야 할 땐 이 switch 문을 반드시 수정해야 한다.
  • 잊는 것을 방지하려면 Class.forName을 사용하는 것이 좋다.
  • 우선 매개변수의 타입을 수정해야 한다.
  • 매개변수 타입 변경은 기본적으로 메서드명 변경을 변형한 것이다.
  • 먼저 인자로 문자열을 받는 새 메서드를 작성하자.
static Employee create(String name) {
    try {
        return (Employee) Class.forName(name).newInstance();
    } catch (Exception e) {
        throw new IllegalArgumentException("객체 " + name + "를 인스턴스화할 없음");
    }
}
  • 그런 다음 정수 타입을 매개변수로 받는 create 메서드의 코드를 앞의 새 메서드 호출로 바꾸자.
static Employee create(int type) {
    switch (type) {
        case ENGINEER:
            return create("Engineer");
        case SALESMAN:
            return create("Salesman");
        case MANAGER:
            return create("Manager");
        default:
            throw new IllegalArgumentException("없는 분류 부호 값");
    }
}
  • 이제 create 메서드 호출 부분에서 다음과 같은 명령문을 찾아 고치자.
Employee eng = Employee.create(Employee.ENGINEER);
Employee eng = Employee.create("Engineer");
  • 여기까지 마치면 정수 타입의 인자를 받는 create 메서드를 없앨 수 있다.

  • 이 방법은 Employee 클래스의 새 하위클래스를 추가할 때 create 메서드를 업데이트할 필요가 없어서 좋다.
  • 그러나 이 방법은 컴파일할 때의 검사가 너무 부족하므로 오타로 인해 런타임 에러가 발생할 수도 있다.
  • 이것이 중요한 문제라면 나는 바로 다음에 설명할 메서드를 작성해 사용하는데, 이때 하위클래스를 하나 추가할 때마다 새 메서드도 추가해야 한다.
  • 유연성과 타입 안전성 사이의 절충점을 찾기 위한 선택의 문제다.
  • 다행히 선택을 잘못하더라도 메서드를 매개변수로 전환이나 매개변수를 메서드로 전환을 적용하면 잘못된 선택을 되돌릴 수 있다.
  • Class.forname을 사용하기가 망설여지는 두 번째 이유는 Class.forName으로 인해 하위클래스 이름이 클라이언트에 노출된다는 것이다.
  • 다른 문자열을 사용해서 팩토리 메서드로는 다른 기능을 수행할 수 있으므로 이건 그다지 단점이라고 할 순 없다.
  • 메서드 내용 직접 삽입을 적용해서 팩토리 메서드를 없애지 않는 이유가 바로 이 때문이다.
12.3.2 예제: 메서드를 사용하는 하위클래스 작성
  • 또 다른 방법은 직접 작성한 메서드를 사용해서 하위클래스를 은폐하는 것이다.
  • 이 방법은 변하지 않는 두세 개의 하위클래스만 있을 때 사용 가능하다.
  • 예컨대, abstract 타입의 Person 클래스가 있고 그 하위 클래스로 Male과 Female이 있다고 하자.
  • 그러면 우선 다음과 같이 각 하위클래스에 해당하는 팩토리 메서드를 정의해야 한다.
public class Person {
	static Person createMale() {
		return new Male();
	}
	
	static Person createFemale() {
		return new Female();
	}
}
  • 생성자 호출 명령은 다음과 같다. 수정하자.
Person kent = new Male();
Person kent = Person.createMale()
  • 이렇게 하면 상위클래스가 하위클래스 정보를 알고 있는 상태가 유지된다.
  • 이를 방지하려면 Product Trader 패턴같은 더 복잡한 기법을 사용해야 한다.
  • 그러나 보통 앞에 두 가지로 충분하다.

13. 하향 타입 변환을 캡슐화

  • 메서드가 반환하는 객체를 호출 부분에서 하향 타입 반환해야 할 땐 하향 타입 변환 기능을 메서드 안으로 옮기자.

13.1 동기

  • 하향 타입 변환은 타입을 철저히 따지는 객체지향 언어에서 제일 귀찮은 일이다.
  • 왜냐하면 하향 타입 변환은 불필요하단 느낌이 드는데다, 컴파일러가 스스로 파악해야 마땅할 것 같은 정보를 개발자가 일일이 알려줘야 하기 때문이다.
  • 하지만 타입을 알아내기 까다로울 때가 많기에 웬만해선 개발자가 직접 작성해야 한다.
  • 사람들이 이터리에터를 어떤 목적으로 사용하는지 살펴보고 그 목적에 맞는 메서드를 작성하자.

13.2 방법

  • 메서드 호출의 결과로 반환된 값을 하향 타입 변환해야 하는 각종 상황을 찾자.
    • 이런 상황은 컬렉션이나 이터레이터를 반환하는 메서드에서 자주 볼 수 있다.
  • 하향 타입 변환 코드를 그 메서드 안으로 옮기자.
    • 컬렉션을 반환하는 메서드에 컬렉션 캡슐화를 적용하자.

13.2 예제

Object lastReading() {
    return readings.lastElement();
}
Reading lastReading() {
	return (Reading) readings.lastElement();
}

14. 에러 부호를 예외 통지로 교체

  • 메서드가 에러를 나타내는 특수한 부호를 반환할 땐 그 부호 반환 코드를 예외 통지 코드로 바꾸자.

14.1 동기

  • 뭔가가 잘못되었을 때 개발자는 그 오류에 대응하는 작업을 실시해야 한다.
  • 에러 찾기 루틴은 에러를 발견하면 자신을 호출한 부분에 그것을 알리며, 호출 부분이 그 에러를 상위 호출 코드로 보낼 수 있다.

14.2 방법

  • 확인된 예외와 미확인 예외 중 어느 것을 사용해야 할지 판단하자.
    • 호출 전에 호출하는 부분이 조건을 검사해야 한다면 미확인 예외로 하자.
    • 예외가 확인된 것이면 새 예외를 작성하거나 기존 예외를 사용하자.
  • 호출 부분을 전부 찾아서 그 예외를 사용하게 수정하자.
    • 미확인 예외일 땐 호출 부분이 메서드 호출 전에 적절한 검사를 하게 하자.
    • 확인된 예외일 땐 호출 부분이 try 절 안에서 메서드를 호출하게 하자.
  • 메서드 시그너처를 수정해서 새로운 용도를 반영하자.

  • 확인된 예외와 미확인 예외 중 어느 것을 사용할지 판단하자.
  • 그 예외를 사용하는 새 메서드를 작성하자.
  • 원본 메서드의 내용을 수정해서 새 메서드를 호출하게 하자.
  • 원본 메서드 호출을 전부 새 메서드 호출로 바꾸자.
  • 원본 메서드를 삭제하자.

14.3 예제

public class Account {

	private int balance;
	
	int withdraw(int amount) {
		if(amount > balance) {
			return -1;
		} else {
			balance -= amount;
			return 0;
		}
	}
}
  • 이 코드가 예외를 사용하게 수정하려면 우선 확인된 예외와 미확인 예외 중 어느 것을 사용할지 정해야 한다.
  • 이 결정은 출금 전의 잔액이 검사하는 기능을 호출 코드가 담당하는지 출금 메서드가 담당하는지에 따라 달라진다.
  • 계좌 잔액 검사가 호출 부분에서 이뤄진다면 withdraw 메서드에 잔액보다 큰 금액을 전달하면서 호출하는 건 프로그래밍 에러다.
  • 프로그래밍 에러, 즉 버그는 미확인 예외를 사용해야 한다.
  • 잔액 검사가 withdraw 메서드에서 이뤄진다면 예외를 반드시 인터페이스 안에 선언해야 한다.
  • 이런 식으로 호출 부분에 예외를 예상하고 적절한 처리를 하게 통지하는 것이다.
14.3.1 미확인 예외
  • 호출 부분이 검사를 담당할 것이다.
  • 반환 코드를 사용하는 부분이 없어야 한다.
  • 그건 프로그래머 에러이기 때문이다.
if(account.withdraw(amount) == -1) {
    handleOverdrawn();
} else {
    doTheUsualThing();
}
  • 다음과 같이 수정한다.
if(!account.canWithdraw(amount)) {
    handleOverdrawn();
} else {
    account.withdraw(amount);
    doTheUsualThing();
}
  • 이제 에러 코드를 삭제하고 해당 에러 상황에 대한 예외를 통지해야 한다.
  • 기능은 정의에 따른다는 점에서 예외적이므로 다음과 같이 조건 검사에 감시 절을 넣어야 한다.
void withdraw(int amount) {
    if(amount > balance) {
        throw new IllegalArgumentException("액수가 너무 큽니다");
    } 

    balance -= amount;
}
  • 이것은 프로그래머 에러이므로 어설션을 넣어 한결 정확하게 표시해야 한다.
void withdraw(int amount) {
    Assert.isTrue("잔액이 충분합니다", amount <= balance);
    balance -= amount;
}

class Assert {
	static void isTrue(String comment, boolean test) {
		if(!test) {
			throw new RuntimeException("어설션 실패: " + comment);
		}
	}
}
14.3.2 확인된 예외
  • 확인된 예외를 사용할 땐 처리 방법이 약간 다르다.
  • 우선 다음과 같이 적당한 새 예외 객체를 작성한다.
class BalanceException extends Exception {
}
  • 호출 부분을 다음과 같이 수정하자.
try {
    account.withdraw(amount);
    doTheUsualThing();
} catch(BalanceException e) {
    handleOverdrawn();
}
  • 이제 withdraw 메서드를 수정해서 앞의 예외를 사용하게 하자.
void withdraw(int amount) throws BalanceException{
    if(amount > balance) {
        throw new BalanceException();
    } 

    balance -= amount;
}
  • 이 과저에서 힘든 점은 메서드와 메서드 호출 부분을 단번에 전부 고쳐야 한다는 것이다.
  • 그러지 않으면 컴파일러 에러가 발생하기 때문이다.
  • 호출 부분이 많을 땐 컴파일과 테스트를 실시하지 않고 하기엔 수정이 너무 방대할 수 있다.
  • 이럴 땐 임시 중개 메서드를 사용하면 된다.
//호출 부분
if(account.withdraw(amount) == -1) {
    handleOverdrawn();
} else {
    doTheUsualThing();
}
//메서드
int withdraw(int amount) {
    if(amount > balance) {
        return -1;
    } else {
        balance -= amount;
        return 0;
    }
}
  • 우선 예외 통지를 사용하는 newWithdraw 메서드를 새로 작성하자.
void newWithdraw(int amount) throws BalanceException {
    if(amount > balance) {
        throw new BalanceException();
    }

    balance -= amount;
}
  • 그리고 원본 메서드 호출을 새 메서드 호출로 고치자.
try {
    account.newWithdraw(amount);
    doTheUsualThing();
} catch (BalanceException e) {
    handleOverdrawn();
}
  • 완료했으면 원본 메서드 삭제하고 메서드명을 원래로 바꾸자.

15. 예외 처리를 테스트로 교체

  • 호출 부분에 사전 검사 코드를 넣으면 될 상황인데 예외 통지를 사용했을 땐 호출 부분이 사전 검사를 실시하게 수정하자.

15.1 동기

  • 예외를 너무 많이 적용하면 좋지 않다.
  • 예외처리는 예외적인 기능, 즉, 예기치 못한 에러에 사용해야 한다.
  • 예외 처리를 조건문 대용으로 사용해선 안 된다.
  • 호출 부분이 메서드를 호출하기 전에 당연히 조건을 검사할 것으로 예상한다면, 개발자는 테스트틀 작성해야 하고 호출 부분은 그 테스트를 사용해야 한다.

15.2 방법

  • 테스트를 앞에 넣고 catch 절의 코드를 if문의 적절한 절로 복사하자.
  • catch 절이 실행되는지 여부가 표시되게 catch 절에 어설션을 넣자.
  • catch절을 삭제하고, 다른 catch 절이 없으면 try 절도 삭제하자.

15.3 예제

  • 새로 생성하기엔 많은 비용이 들지만 재사용할 수 있는 각종 리소스를 관리하는 객체를 사용하겠다.
  • 데이터 베이스 접속이 좋은 예
  • 리소스 관리 객체에는 두 개의 리소스 풀이 있다.
  • 하나는 가용 리소스 풀이고 다른 하는 할당 리소스 풀이다.
  • 클라이언트가 리소스를 요청하면 리소스 관리 객체는 리소스는 넘겨주고 가용 풀에 있던 리소스를 할당 풀로 전달한다.
  • 클라이언트가 리소스를 해제하면 관리 객체는 거꾸로 할당 풀의 리소스를 가용풀로 전달한다.
  • 클라이언트가 리소스를 요청했는데 사용 가능한 리소스가 없으면 관리 객체는 새 리소스를 생성한다.
public class ResourcePool {
	
	Stack<Resource> available;
	Stack<Resource> allocated;
	
	Resource getResource() {
		Resource result;
		
		try {
			result = available.pop();
			allocated.push(result);
			return result;
		} catch(EmptyStackException e) {
			result = new Resource();
			allocated.push(result);
			return result;
		}
	}
}
  • 여기서 리소스 고갈은 예기치 못한 일이 아니므로 예외 처리를 사용하면 안 된다.
  • 예외를 없애려면 우선 적절한 사전 테스트를 넣고 거기에 가용 리소스가 없을 때의 처리 코드를 넣는다.
Resource getResource() {
    Resource result;

    if(available.isEmpty()) {
        result = new Resource();
        allocated.push(result);
        return result;
    } else {
        try {
            result = available.pop();
            allocated.push(result);
            return result;
        } catch(EmptyStackException e) {
            result = new Resource();
            allocated.push(result);
            return result;
        }
    }
}
  • 앞의 코드에선 예외가 절대로 발생하지 않아야 한다.
  • 거걸 확실히 하고자 다음과 같이 어설션을 넣는다.
public class ResourcePool {
	
	Stack<Resource> available;
	Stack<Resource> allocated;
	
	Resource getResource() {
		Resource result;
		
		if(available.isEmpty()) {
			result = new Resource();
			allocated.push(result);
			return result;
		} else {
			try {
				result = available.pop();
				allocated.push(result);
				return result;
			} catch(EmptyStackException e) {
				Assert.shouldNeverReachHere("pop 실행 시에 available이 비어 있음");
				result = new Resource();
				allocated.push(result);
				return result;
			}
		}
	}
}

class Assert {
	static void shouldNeverReachHere(String message) {
		throw new RuntimeException(message);
	}
}
  • 컴파일 후 예외가 발생하지 않으면 try절 삭제한다.
public class ResourcePool {
	
	Stack<Resource> available;
	Stack<Resource> allocated;
	
	Resource getResource() {
		Resource result;
		
		if(available.isEmpty()) {
			result = new Resource();
		} else {
			result = available.pop();
		}
		
		allocated.push(result);
		return result;
	}
}