메소드와 생성자의 입력 매개변수 값에 제약이 있다면 반드시 문서화해야 하고, 메소드 몸체 시작 전 검사해야한다.
매개변수 검사를 제대로 하지 못하면 다음과 같은 문제가 있다.
- 메소드가 수행되며 모호한 예외를 던지며 실패한다.
- 메소드는 수행되며 잘못된 결과를 반환한다
- 메소드가 수행되며 반환된 잘못된 결과가 미래 알 수 없는 시점에 메소드와 관련없는 오류를 발생시킨다.
즉, 매개변수 검사에 실패하면 실패 원자성(failure atomicity)을 어기는 결과가 발생한다.
*실패 원자성(failure atomicity): 호출된 메소드가 실패하더라도 해당 객체는 메소드 호출 전 상태를 가져야하는 성질(item76)
잘못된 매개변수의 검사 예외
public과 protected 메소드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화하자(javadoc 태그 @throws이용, item76)
매개변수의 제약을 문서화한다면, 그 제약을 어겼을 때 발생하는 예외도 함께 기술해야 API 사용자가 제약을 지킬 가능성이 높아진다.

mod 메소드의 주석을 보면 항상 음이 아닌 BigInteger를 반환한다는 점에서 remainder 메소드와 다르며, 현재 값을 파라미터 m으로 나눈 나머지를 반환한다는 것을 볼 수 있다. 그리고 m이 음수라면 ArithmeticException을 던진다고 기술되어있다.
하지만 BigInteger인 m이 null이라면 m.signum을 호출할 때 NullPointerException이 발생할텐데 이런 주석은 어디에도 없다.
이유는 이 설명을 개별 메소드가 아닌 BigInteger 클래스 수준에서 기술했기 때문이다. 클래스 수준 주석은 그 클래스의 모든 public 메소드에 적용되므로 각 메소드에 일일이 기술하는 것보다 훨씬 깔끔한 방법이다.
null을 처리하는 애노테이션도 같은 목적이라도 여러 패키지가 존재한다.
IDE와 javax.validation
IDE에서 @Nullable이나 @NotNull 애노테이션이 있지만, compile 타임에서 default로 warning만 준다.
이 설정을 변경할수도 있지만, 런타임에서 예외를 던지는거도 아니고 표준적인 방법이 아니다.
javax.validation도 @NotNull이 있지만, 역시 런타임에서 예외를 던져주지 않았고 단지 warning을 하이라이트만 해준다.
참고: Intellij의 @Nullable, @NotNull / javx.validation의 @NotNull / javax는?
Lombok 활용
Lombok은 애노테이션 기반 코드 자동완성을 도와주는 라이브러리다. 그 중에 @NonNull이 있었고 이 애노테이션은 null인 경우 런타임에서 NullPointerException을 던졌다! 하지만 자바에서 제공하는게 아니니 표준적인 방법은 아니다.
참고: Lombok gradle 설치
(번외) Optional 활용
객체를 Optional에 담아 null이면 예외를 던지게도 작성할 수 있다! 근데 활용하지않고 단순히 null인지 확인만 하는거면 아래 Objects의 정적 메소드들을 쓰는게 나을 것 같다. Wrapping을 한 후 새 객체로 만들어 확인하니까! 자세한 건 아래 코드를 참조하자!
추가로 알아본 여러가지 null 처리 방법들이 있었다!
하지만 표준적이지 않고 단순 null 체크를 위해 Optional을 사용하기에도 무거운 방법들이다.
코드들로 테스트한 결과는 아래 소스를 참고하자. CheckingXXX 형태로 다양한 객체를 만들었을 때 IDE에서 확인하자.
GitHub - kkt219a/various-experiments: 다양한 실험들
:wrench: 다양한 실험들 . Contribute to kkt219a/various-experiments development by creating an account on GitHub.
github.com
java.util.Objects
자바 7에 추가된 Objects의 requireNonNull 정적 메소드는 유연하고 사용하기 편하니 null 검사를 수동으로 하지 않아도 된다.
첫 번째 인자로 null인지 확인할 객체를 넣으면 된다. 다중정의되어 있기 때문에 두 번째 인자는 메시지일 뿐이라 NPE는 전부 터진다.

추가로 null과 관련된 어떤 정적 메소드들이 있는지 궁금해서 Objects.null을 타이핑해서 자동완성되는 것을 확인했다.

isNull, nonNull로 단순 null인지 여부를 boolean으로 반환해주는 메소드와, 마치 Optional 처럼 null일 경우 두 번째 인자로 인스턴스를 넘기거나, 인스턴스를 만들 수 있는 Supplier도 제공하는 requireNonNullElse(Get) 메소드도 제공한다. 이런 메소드들을 사용할거면 Optional이 더 유연해보인다!
Objects엔 자바 9부터 범위 검사 기능도 더해졌다

하지만 예외 메시지도 지정할 수 없고 리스트와 배열 전용으로 설계됐다. 그리고 닫힌 범위(closed range; 양 끝단 값을 포함하는)는 다루지 못한다. 그래도 제약이 걸림돌이 되지 않는 상황에서는 유용하다!

이외의 메소드들에는 기본적인 equals, hasCode, toString, compare 등 메소드들이 있었는데,
전체적으로 null인지 확인한 상태에서 자신의 함수를 다시 호출해주는 로직이었다.
단언문(assert) 사용
jdk 1.4부터 assert라는 예약어를 지원한다. public이 아닌 메소드라면 유효한 값만 넘겨지도록 보증하며 통제해야 한다.
그 때 assert를 사용해 매개변수 유효성을 검증할 수 있다.
protected static void callAssert(Integer value, Integer value2) {
assert value!=null;
assert value2 !=null;
}
각 단언문을 테스트하기 위해 2개의 assert를 선언했다. assert는 자신이 단언한 조건이 무조건 참이라고 선언하는 것이다.
단언문은 일반적인 유효성 검사와 다른 점이 있다.
- 실패하면 AssertionError를 던진다.
- 런타임에 아무런 효과도, 아무런 성능 저하도 없다.
2번은 java를 실행할 떄 명령줄에서 -ea 나 --enableassertions 플래그를 설정하면 런타임에 영향을 줄 수 있다.
혹은 IDE에서도 설정할 수 있다. 원래는 IntelliJ에 [Help - Edit Custom VM Options..]에 자동으로 -ea가 포함되서
런타임에 영향을 끼쳐야하는데 test 코드에 대해서만 AssertionError을 던졌다
아마 저렇게 설정된 게 Test 코드에만 영향을 끼치는 것 같았고, 내가 사용할 클래스를 따로 지정해야할 것 같았다.
[Run - edit Configurations...] 에 들어가서, 내가 실행하는 메인문이 존재하는 클래스 파일에 VM 옵션을 직접 추가했다.

이렇게 설정을 하면 런타임에 테스트 외 지정 클래스에 예외를 던지게할 수 있다.
매개변수를 이후 시점에 사용하는 경우
아이템 20에서 사용했던 골격 구현 구체 클래스 코드를 보자.
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
return new AbstractList<>() {
@Override public Integer get(int i) {
return a[i]; // 오토박싱(아이템 6)
}
@Override public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val; // 오토언박싱
return oldVal; // 오토박싱
}
@Override public int size() {
return a.length;
}
};
}
Objects.requireNonNull 메소드가 null 검사를 수행해서 null이면 예외를 던졌는데, 만약 생략했다면
새로 생선한 List 인스턴스를 클라이언트가 사용하려고 메소드를 호출하는 시점에서 NPE가 발생한다.
어디서 가져왔는지 추적도 어렵고 디버깅이 아주 힘들어지는 것!
특히나 생성자는 나중에 쓰기 위해 저장하기 때문에 불변식을 어기는 객체가 만들어지지 않게 해야한다.
메소드 매개변수 유효성 검사의 예외적인 상황
- 유효성 검사 비용이 지나치게 높을 때
- 실용적이지 않을 때
- 계산 과정에서 암묵적으로 검사가 수행될 때
3번의 예시로 Collections.sort(List)를 보자. 리스트 안의 객체들은 모두 상호 비교될 수 있어야하고 정렬 과정에서 비교가 이뤄진다.
만약 상호 비교될 수 없는 타입의 객체가 들어 있다면 ClassCastException을 던진다.
따라서 모든 객체가 상호 비교될 수 있는지의 검사는 별 실익이 없다. 하지만 암묵적 검사에 너무 의존하면 실패 원자성을 해치니 주의하자.
그리고 계산 과정에서 필요한 유효성 검사가 이뤄지지만 실패 시 잘못된 예외를 던지기도 한다.
계산 중 잘못된 매개변수 값을 사용해 발생한 예외와 API 문서에서 던지기로 한 예외가 다를 수 있는 것.
따라서 예외 번역(exception translate) 관요우를 사용해 API 문서에 기재된 예외로 번역해줘야한다. --> 아이템 73 참고
메소드는 최대한 범용적으로 설계해야 하고 매개변수 제약은 적을수록 좋지만, 구현 개념 자체가 특정 제약을 내재한 경우도 존재한다.
메소드나 생성자를 작성할 때 매개변수 제약을 문서화하자. 메소드 코드 시작 부에서 명시적 검사를 하는 습관을 기르자.
* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.
'Reading > Effective Java' 카테고리의 다른 글
| [Effective-Java] Item 51. 메소드 시그니처를 신중히 설계하라 (0) | 2021.11.08 |
|---|---|
| [Effective-Java] Item 50. 적시에 방어적 복사본을 만들라 (0) | 2021.11.08 |
| [Effective-Java] Item 48. 스트림 병렬화는 주의해서 적용하라 (0) | 2021.11.08 |
| [Effective-Java] Item 47. 반환 타입으로는 스트림보다 컬렉션이 낫다 (0) | 2021.11.08 |
| [Effective-Java] Item 46. 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2021.11.01 |