이 장에서는 매개 변수와 return 값을 처리하는 방법, 메소드 시그니처(signature)를 설계하는 방법, 메소드를 문서화하는 방법에 대해서 설명한다.
매개 변수가 유효한지 검사하자
대부분의 메소드와 생성자는 자신들의 매개변수로 전달될 수 있는 값에 제한을 둔다.
예를 들면, 배열의 인덱스는 음수가 아니어야 하고 객체 참조는 null이 아니어야 한다는 것들이다.
이런 모든 제약은 명확하게 문서화해야 하며, 메소드 몸체 코드의 맨 앞에서 검사하도록 해야한다.
public 메소드의 경우는 Javadoc의 @throws 태그를 사용해서 매개 변수의 값의 제약을 위반했을 때 발생되는 예외를 문서화 한다(항목 62).
일반적으로 IllegalArgumentException, IndexOutOfBoundsException, NullPointerException 예외가 될 것이다 (항목 60).
// BigInteger.java 일부 발췌
/**
* 그 값이 (this mod m)인 BigInteger를 반환한다.
* 이 메소드는 항상 양수의 BigInteger를 반환하는
* BigInteger의 remainder 메소드와는 다르다.
*
* @param m은 계수(modulus)로서, 반드시 양수여야 한다.
* @return this mod m
* @throws ArithmeticException 만일 m이 0보다 작거나 같으면
*/
public BigInteger mod(BigInteger m) {
if (m.signum <= 0)
throw new ArithmeticException("BigInteger: modulus not positive");
BigInteger result = this.remainder(m);
return (result.signum >= 0 ? result : result.add(m));
}
필요하면 방어 복사본을 만들자
자바는 안전한 언어(safe language)이기 때문에 메모리 훼손 에러로부터 자유롭다.
그러나 안전한 언어일지라도, 노력 없이 다른 클래스와 독립적인 클래스를 만드는 것은 어려운 일이다.
우리는 클라이언트가 불변 규칙을 파괴할 수도 있다라는 가정하에 방어적으로 프로그램을 작성해야 한다.
// 결함이 있는 "불변" Period 클래스
import java.util.*;
public final class Period {
private final Date start;
private final Date end;
/**
* @param start 시작일
* @param end 종료일, 시작일보다 빠르면 안된다.
* @throws IllegalArgumentException 시작일이 종료일보다 늦으면 발생
* @throws NullPointerException 시작일이나 종료일이 null이면 발생
*/
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(
start + " after " + end);
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
public String toString() {
return start + " - " + end;
}
// 나머지 코드 생략
}
Period 클래스를 얼핏보면 불변이면서 시작일이 종료일보다 늦지 않는다는 불변 규칙을 지키도록 하는 것 같이 보인다.
그러나 Date가 가변 객체라는 것을 이용해서 쉽게 불변 규칙을 깰 수 있다.
import java.util.*;
public class Attack {
public static void main(String[] args) {
// 첫번째 공격 - Period 인스턴스의 내부를 공격한다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부를 변경한다!
System.out.println(p);
}
}
위와 같은 공격으로 부터 Period 인스턴스 내부를 보호하려면, 가변 객체인 매개 변수의 방어 복사본(defecsive copy)을 만들어서 생성자에 전달해야 한다.
// 수정된 생성자 - 매개 변수의 방어 복사본을 만든다.
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(start +" after "+ end);
}
방어 복사본은 매개 변수의 유효성 검사에 앞서 만들어야 하며(항목 38), 유효성 검사는 원본이 아닌 복사본을 대상으로 한다는 것에 유의하자.
매개 변수가 검사되는 시간과 복제되는 시간 사이에 다른 쓰레드가 매개 변수를 변경하는 것으로부터 클래스를 보호해준다.
위에서 교체한 생성자가 공격을 방어한다고 해도 Period 인스턴스가 변경될 가능성은 여전히 있다.
import java.util.*;
public class Attack {
public static void main(String[] args) {
// 두번째 공격 - Period 인스턴스의 내부를 공격한다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // p의 내부를 변경한다!
System.out.println(p);
}
}
Period의 접근자(accessor) 메소드에서 자신의 내부 가변 필드들에 대한 접근을 제공하고 있기 때문이다.
두번째 공격을 방어하려면 접근자 메소드를 수정하여 내부 가변 필드의 방어 복사본을 반환하도록 하면 된다.
// 수정된 접근자 메소드 - 내부 필드의 방어 복사본을 만든다.
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
매개 변수의 방어 복사는 불변 클래스를 위한 것만은 아니다.
클라이언트가 제공하는 객체를 내부 데이터 구조로 넣는 메소드나 생성자를 작성할 때는 언제든지 해당되므로, 이 경우 제공하는 객체가 가변적이 될 수 있는지 잘 생각하자.
내부 컴포넌트를 클라이언트에게 반환할 때에도 방어 복사를 해야한다.
우리 클래스가 불변이건 아니건, 가변적일 수 있는 내부 컴포넌트의 객체 참조를 반환할 때는 방어 복사를 반환해야 한다.
그러나 방어 복사 비용이 엄청나게 비싸면서, 클라이언트가 컴포넌트를 변경하지 않을 것이라는 신뢰가 있으면 방어 복사를 하지 않아도 된다.
메소드 시그니처를 신중하게 설계하자
이 항목은 낱개로 논할 정도의 가치가 없는 API 설계 힌트들을 모아놓은 것이므로 간단하게 훓고 넘어가자.
메소드 이름을 신중하게 짓자. 이름은 항상 표준 작명 규칙(standard naming convention)을 따라야 한다 (항목 56).
편리한 메소드를 만드는 것에 너무 열중하지 말자. 메소드가 너무 많으면, 클래스를 배우고 사용하고 문서화하고 테스트하고 유지하기가 어렵게 된다.
너무 많은 매개 변수를 피하자. 매개변수는 4개 이하를 목표로 하자. 긴 매개변수를 줄이는 방법 1. 하나의 메소드를 여러 개로 쪼갠다. 2. 매개 변수 그룹들을 보전하는 지원 클래스(helper class)를 만든다. 일반적으로 지원 클래스들은 static 멤버 클래스이다 (항목 22). 3. 객체 구축에서부터 메소드 호출까지 빌더 패턴(항목 2)을 적용한다.
매개 변수 타입은 클래스보다 인터페이스를 사용하자 (항목 52).
boolean 매개 변수 보다는 2개의 요소를 갖는 enum을 사용하자. 읽기와 작성이 더 쉬운 코드를 작성할 수 있다.
public enum TemperatureScale { FAHRENHEIT, CELSIUS} // 화씨, 섭씨
Thermometer.newInstance(true) 보다는 Thermometer.newInstance(TemperatureScale.CELSIUS)가 더 좋다.
향후 버전에서는 Thermometer에 새로운 static 팩토리 메소드를 추가하지 않고도 KELVIN을 TemperatureScale 에 추가할 수 있다.
또 온도 눈금(temperature scale)간의 관계를 enum 메소드로 만들 수 있다 (항목 30).
예를 들어 각 온도 눈금 상수는 double 타입의 값을 매개변수로 받아 섭씨(Celsious) 온도로 맞추는 메소드를 가질 수 있다.
오버로딩(overloading)을 분별력 있게 사용하자
classify 메소드의 인자에 따라 컬렉션을 분류하는 다음 프로그램을 보자.
// 결함이 있다! - 이 프로그램은 무엇을 출력할까?
import java.util.*;
import java.math.*;
public class CollectionClassifier {
public static String classify(Set> s) {
return "Set";
}
public static String classify(List> lst) {
return "List";
}
public static String classify(Collection> c) {
return "Unknown Collection";
}
public static void main(String[] args) {
Collection>[] collections = {
new HashSet(),
new ArrayList(),
new HashMap().values()
};
for (Collection> c : collections)
System.out.println(classify(c));
}
}
---------- execute ----------
Unknown Collection
Unknown Collection
Unknown Collection
이 프로그램을 실행하면 Set List Unknown Collection 순으로 출력될 것 같지만 그렇지 않다.
왜냐하면, classify 메소드가 오버로딩되어서, 호출될 메소드가 컴파일 시점에 결정되기 때문이다.
런타임 타입은 각각 다르지만, 컴파일 시 타입이 동일하게 Collection> c이므로 Unknown Collection이 세번 출력되는 것이다.
creative by javacafe