아이템 49: 매개변수가 유효한지 검사하라
메서드나 생성자를 작성할 때에는 매개변수에 어떤 제약이 있을지 생각해야 한다.
- 제약사항을 문서화하고 메서드 시작 부분에 명시적으로 검사하자.
- 유효성 검사를 제대로 하지 않는 경우에는 중간에 모호한 오류가 발생하거나, 잘못된 결과를 반환하거나, 실패 원자성이 깨지는 결과를 낳을 수 있다.
- public과 protected 메서드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화해야 한다.
/**
* (현재 값 mod m) 값을 반환한다. 이 메서드는
* 항상 음이 아닌 BigInteger를 반환한다는 점에서 remainder 메서드와 다르다.
*
* @param m 계수(양수여야 한다.)
* @return 현재 값 mod m
* @throws ArithmeticException m이 0보다 작거나 같으면 발생한다.
*/
public BigInteger mod(BigInteger m) {
if (m.signum() <= 0)
throw new ArithmeticException("계수(m)는 양수여야 합니다. " + m);
... // 계산 수행
}
자바 7부터는 Objects.requireNonNull
메서드 등을 사용하면 null 검사를 편하게 할 수 있다.
this.strategy = Objects.requireNonNull(strategy, "전략");
나중에 쓰기 위해 저장하는 매개변수는 특히 더 신경써야 한다.
→ 저장 시점에 오류가 발생하지 않기 때문
메서드 몸체 실행 전 매개변수 유효성 검사의 예외
- 유효성 검사 비용이 지나치게 높은 경우
- 계산 과정에서 암묵적으로 유효성 검사가 실행되는 경우
- 메서드 수행 중 API에서 던지기로 한 예외와 다른 예외가 발생하는 경우 API에서 던지기로 한 예외로 번역해주어야 한다.
아이템 50: 적시에 방어적 복사본을 만들라
클래스가 클라이언트로부터 받는 매개변수가 가변 객체인 경우 방어적 복사를 통해 클래스 내부를 보호해야 한다.
가변 클래스인 java.lang.Date를 매개변수로 받는 Period 클래스
// 코드 50-1 기간을 표현하는 클래스 - 불변식을 지키지 못했다. (302-305쪽)
public final class Period {
private final Date start;
private final Date end;
/**
* @param start 시작 시각
* @param end 종료 시각. 시작 시각보다 뒤여야 한다.
* @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
* @throws NullPointerException start나 end가 null이면 발생한다.
*/
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(
start + "가 " + end + "보다 늦다.");
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
public String toString() {
return start + " - " + end;
}
}
가변 매개변수를 이용한 공격
// 코드 50-2 Period 인스턴스의 내부를 공격해보자. (303쪽)
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부를 변경했다!
System.out.println(p);
이와 같은 공격을 예방하려면 적시에 객체의 방어적 복사본을 만들어야 한다.
→ 이때 clone
메서드는 final이 아닌 타입의 경우 재정의할 수 있기 때문에 사용해서는 안 된다.
가변 반환값을 이용한 공격
// 코드 50-4 Period 인스턴스를 향한 두 번째 공격 (305쪽)
start = new Date();
end = new Date();
p = new Period(start, end);
p.end().setYear(78); // p의 내부를 변경했다!
System.out.println(p);
이 경우에도 동일하게 가변 필드의 방어적 복사본을 만들어 반환하면 된다.
→ 인스턴스 필드에 있는 객체의 타입은 보장되기 때문에 clone 메서드를 사용해도 관계없다. (하지만 굳이 사용하지는 말자)
아이템 51: 메서드 시그니처를 신중히 설계하라
- 메서드 이름을 신중히 짓자.
- 항상 표준 명명 규칙을 따르고, 같은 패키지의 다른 이름들과 일관되게 짓자.
- 너무 긴 이름은 피하자.
- 편의 메서드를 너무 많이 만들지 말자.
- 매개변수 목록은 짧게 유지하자.
- 같은 타입의 매개변수 여러 개가 연달아 나오는 경우가 최악이다.
- 매개변수 목록이 길 때 사용할 수 있는 방법
- 여러 메서드로 쪼개기
- e.g. subList() and indexOf()
- 매개변수 여러 개를 묶어주는 헬퍼 클래스 만들기
- e.g. XXXCriteria
- 빌더 패턴을 메서드 호출에 응용하기
- 여러 메서드로 쪼개기
- 매개변수 타입으로는 가능하다면 인터페이스를 사용하자.
- ‘진짜’ boolean이 아닌 이상 boolean보다는 enum이 낫다.
아이템 52: 다중정의는 신중히 사용하라
- 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택된다.
- 어떤 다중정의한 메서드를 사용할지는 컴파일 타임에 정해진다.
// 코드 52-1 컬렉션 분류기 - 오류! 이 프로그램은 무엇을 출력할까? (312쪽)
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "집합";
}
public static String classify(List<?> lst) {
return "리스트";
}
public static String classify(Collection<?> c) {
return "그 외";
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values()
};
for (Collection<?> c : collections)
System.out.println(classify(c));
}
}
- 다중정의가 혼동을 일으키는 상황을 피해야 한다.
- 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들면 안 된다.
- 다중정의하는 대신 메서드 이름을 다르게 지어줄 수도 있다.
- e.g. readBoolean(), readInt(), readLong(), …
- 생성자의 경우에는 어쩔 수 없지만 정적 팩터리 메서드를 활용하는 방식도 있다.
- 메서드 다중정의 시 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안 된다.
아이템 53: 가변인수는 신중히 사용하라
가변인수 사용 시 성능 문제에 주의하자.
→ 가변인수 메서드는 호출될 때마다 배열을 생성하기 때문이다.
- 인수가 n개 이상이어야 할 경우, n개까지는 직접 명시하고 이후부터 가변인수를 사용하자.
- 성능에 민감한 상황이라면, 호출 빈도를 고려해 자주 사용되는 인수 개수의 메서드는 다중정의해 놓자.
아이템 54: null이 아닌, 빈 컬렉션이나 배열을 반환하라
컬렉션이나 배열을 반환하도록 되어 있는 메서드의 경우 null을 반환하면 사용처에서 추가로 null 체크 로직이 추가되어야 해서 복잡도가 늘어난다.
- 빈 컬렉션이나 배열을 반환하자.
- 성능 문제가 걱정된다면 캐시된 불변 객체를 반환하도록 만들 수도 있다.
아이템 55: 옵셔널 반환은 신중히 하라
자바 8부터 제공되는 옵셔널은 명시적으로 반환값이 없을 수도 있다고 선언하는 효과를 준다.
옵셔널의 활용방법
- 기본값을 정해둔다.
orElse(XXX)
- 기본값 설정비용이 큰 경우 미리 생성하지 않고 supplier 함수를 제공할 수도 있다.
- 원하는 예외를 던진다.
orElseThrow()
- 항상 값이 채워져 있다고 가정한다.
get()
옵셔널 사용 시 주의사항
- 옵셔널을 반환하도록 선언되어 있는 메서드는 절대 null을 반환해서는 안 된다.
- 컬렉션, 스트림, 배열, 옵셔널 등의 컨테이너 타입은 옵셔널로 감싸면 안 된다.
- 박싱된 기본 타입을 옵셔널로 감싸기보단, 전용 옵셔널 클래스를 사용하자.
- 옵셔널을 컬렉션의 키, 값, 원소로 사용하는 것은 부적절하다.
- 빈 값을 표현하는 방법이 두 가지가 되기 때문
아이템 56: 공개된 API 요소에는 항상 문서화 주석을 작성하라
문서화 주석의 원칙
- API를 올바로 문서화하기 위해서는 모든 클래스와 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 한다.
- 메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 명료하게 기술해야 한다.
- how가 아닌 what
- 호출을 위한 전제조건, 부작용 등
문서화 주석 작성 주의사항
- 문서화 주석의 요약 설명은 반드시 대상의 기능을 고유하게 기술해야 한다.
- 한 클래스 안에서 요약 설명이 똑같은 멤버가 둘 이상이면 안 된다.
- 제네릭 타입이나 메서드를 문서화할 때는 모든 타입 매개변수에 주석을 달아야 한다.
- 열거 타입을 문서화할 때는 상수에도 주석을 달아야 한다.
- 애너테이션 타입을 문서화할 때는 멤버들에도 모두 주석을 달아야 한다.
- 클래스 혹은 정적 메서드가 스레드 안전하든 그렇지 않든, 스레드 안전 수준을 API 설명에 포함해야 한다.
javadoc으로 생성된 문서 페이지를 직접 읽어보고 잘 작성된 문서화 페이지인지 점검하자.