Description
Discussed in https://github.com/orgs/Study-2-Effective-Java/discussions/114
Originally posted by bunsung92 February 10, 2023
📝 구성
1. 매개변수가 유효한지 검사하라
메서드와 생성자 대부분은 입력값이 특정 조건을 만족하기를 바란다.
private List<String> names;
// ...
public String getNameOf(int index) {
return names.get(index);
}
위와 같이 이름을 관리하고 있는 클래스가 있고 관리하고 있는 이름의 인덱스값을 통해 값을 조회하는 getNameOf
가 있다고 가정한다. 이때 index
가 음수이거나 names
의 크기보다 크다면 에러가 발생한다.
이런 제약은 반드시 문서화해야하며 메서드 바디가 실행되기 전에 검증을 해야한다.
오류는 가능한 한 빨리 잡아야한다.
오류를 발생한 시점에서 바로 잡지 못하면 해당 오류를 감지하기 어려워지고 오류의 발생 지점을 찾기 힘들어진다.
따라서 메서드 바디가 실행되기 전에 매개변수를 확인한다면 잘못된 값이 넘어왔을때 즉각적이고 깔끔한 방식으로 예외를 던질 수 있다.
2. 왜 매개변수를 검증해야 하는가?
매개변수 검증이 이뤄지지 않는다면 메서드가 실행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
또는 메서드가 제대로 수행 되더라도 예상치 못한 결과를 도출할 수 있다.
특히 후자의 경우가 문제가 되는데 전자는 예외를 던지며 에러로그를 남기면 해결할 수 있지만, 후자는 실제 데이터 정합성이 깨진것을 확인했을때 인지가 가능하기 때문이다.
이렇게 매개변수 검증에 실패하면 실패하면 실패 원자성
을 어기는 결과가 나타난다.
- 실패 원자성은 item76에서 다룸
호출된 메서드가 실패하더라도 해당 객체는 호출 전의 상태를 유지해야한다.
3. 매개변수 검증 방법
3.1 public과 protected 메서드는 던지는 예외를 문서화해야한다.
public
과 protected
는 외부 접근도가 높은 제어자이므로 이들 메서드에는 @throws
Javadoc 태그를 통해 문서화해야한다.
/**
* Returns a BigInteger whose value is {@code (this mod m}). This method
* differs from {@code remainder} in that it always returns a
* <i>non-negative</i> BigInteger.
*
* @param m the modulus.
* @return {@code this mod m}
* @throws ArithmeticException {@code m} ≤ 0
* @see #remainder
*/
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));
}
위는 BigInteger
의 mod()
로 매개변수 m이 0 이하일때 ArithmeticException
이 발생하는 것을
@throws
Javadoc 태그로 명시하고 있다.
참고로 모든 메서드에 적용되는 문서화를 하고 싶다면 클래스 수준에서 Javadoc 을 작성하면 된다.
public
, protected
이외에 package pirvate
, private
는 개발자가 스스로 메서드가 호출되는 상황을 통제할 수 있다. 즉, 유효한 값이 들어온다는 것을 보장할 수 있다.
3.2 assert
public
, protected
메서드 이외에는 assert
단언문을 통해서 매개변수 유효성을 검증할 수 있다.
private String get(int index) {
assert index >= 0;
return names.get(index);
}
assert
단언문은 자신이 선언한 조건이 무조건 참이어야 다음 로직을 수행한다.
그렇지 않으면 AssertionException
을 던진다.
assert
는 Java 실행시 -ea 또는 --enableassertions
옵션을 주지않으면 런타임에 아무런 영향을 주지 않으며 성능 저하도 없다.
보통 개발 중에 테스팅하는 목적으로 사용된다.
3.3 매개변수와 생성자
메서드에서 직접 사용하지는 않지만 나중에 사용하기 위해서 필드로 저장하는 경우
는 더더욱 철저히 검사가 필요하다. 이때 검사가 제대로 이뤄지지 않으면 null
이 필드에 할당되거나 컬렉션이라면 컬렉션 안에 null
이 들어갈 수 있다. 그리고 이 null
을 사용함으로써 NullPointerException
이 발생할 수 있다.
NullPointerException
은 디버깅이 힘들다.
생성자는 나중에 쓰려고 저장하는 매개변수의 유효성을 검사하라 원칙의 특수사례이다. 생성자 매개변수의 유효성 검사는 클래스 불변식을 어기는 객체가 만들어지지 않게 하는데 필수이다.
3.4 그외
유효성 검사 비용이 지나치게 높거나 실용적이지 않다면 예외적으로 유효성 검사에 대해 고려해 볼 수있다.
대신 Exception에 대한 대응 방안을 반드시 두어야 할 것
암묵적으로 유효성 검사를 하는 경우
Collection
의 sort()
처럼 컬렉션에 들어있는 객체의 타입이 다르면 ClassCastException
이 발생한다. 로직이 수행되면서 유효성에 어긋나는 경우 Exception
을 던지므로 실행 전에 유효성 검사를 수행할 필요는 없다.
다만, 암묵적 유효성 검사에 너무 의존하면 실패 원자성을 해칠 수 있다.
4. What about null parameters?
Effective Java에서는 Java 7에 추가된 java.util.Objects.requireNonNull
를 유연하고 사용하기 편한 메서드로 소개하고 있다.
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
java.util.Objects.requireNonNull
도 결국에는 매개변수 obj
가 null이라면 그대로 NullPointerException
을 일으킨다.
java.util.Objects.requireNonNull
을 활용한다면 다음과 같이 null일 때 default 값을 반환할 수 있도록 만들어 보는 활용 방안을 생각해 볼 수있다.
public static <T> T requireNonNullElse(T obj, T defaultObj) {
try {
Objects.requireNonNull(obj)
return obj;
} catch(NullPointerException e) {
log.error("{} is null", obj);
return defaultObj;
}
}
Java9에는 requireNonNullElse 를 지원하여 Java API에서 지원한다.
Java 9에는
requireNonNullElse
이외에도checkFromIndexSize
,checkFromToIndex
,
checkIndex
등 다양한 검사가 추가 되었다.