Effective Java

[Effective Java] 아이템 49 - 매개변수가 유효한지 검사하라

로드태환 2022. 4. 5. 15:15

메서드와 생성자 대부분 입력 매개변수의 값이 특정 조건을 만족하기를 바란다. 이런 제약은 문서화가 필요하며 메서드 몸체가 시작되기 전에 검사해야한다. 이는 "오류는 가능한 빨리(발생한 곳에서) 잡아야한다."는 일반 원칙의 사례이기도 하다. 

메서드 몸체가 실행되기 전에 매개변수를 확인하면 즉각적이고 깔끔한 방식으로 예외를 던질 수 있다.

 

반대로 매개변수 검사를 제대로 수행하지 못하면 아래와 같은 문제가 발생한다.

  1. 메서드가 수행되는 중간에 모호한 예외를 던지며 실패한다. 
  2.  메서드는 잘 수행되지만 잘못된 결과를 반환한다.
  3. 메서드는 잘 수해되지만 어떤 객체를 이상한 상태로 만들어 놓아 알수 없는 시점에 이 메서드와 관련없는 오류를 낸다.

즉 매개변수 검사에 실패하면 실패 원자성(failure atomicity)를 어기게 된다.

 

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);
   ...
}

이 메서드는 m이 null 일때 NullPointerException을 던진다. 하지만 이는 코드나 문서에 어디에도 없다. 이 설명을 개별 메서드가 아닌 BigInteger 클래스 수준에서 기술했기 때문이다. 클래스 수준 주석은 그 클래스의 모든 public 메서드에 적용되어 훨씬 깔끔하다.

@Nullable 로 널 가능을 알려줄 수 있지만 표준적인 방법은 아니다.

 

자바 7에 추가된 java.util.Object.requireNonNull 메서드는 유연하고 사용하기 편해, 사용하게 된다면 null 검사를 명시적으로 하지 않아도 된다. Ex) this.strategy = Object.requireNonNull(strategy, "전략");

자바 9에서는 null 뿐만 아니라 Object에 범위 검사도 가능해 졌다. '예외 메세지 지정 불가', '리스트와 배열 전용', '닫힌 범위 불가' 등  null 만큼 유연하진 않지만 대부분의 경우에서 유용하다.

 

 public이 아닌 메서드라면 오직 유효한 값 만이 메서드에 넘겨진다는 것을 assert를 사용해 보증할 수 있고 그래야 한다.

private static void sort(long[] a, int offset, int length) {
 assert a != null;
 assert offset >= 0 && offset <= a.length;
 assert length >= 0 && length <= a.length - offset;
   ...
}

단언문은 유효성 검사와 달리 실패하면 AssertException을 던지고, 런타임에 아무런 영향도 없다.

 

 

static List<Integer> intArrayAsList(int[] a) {
 Objects.requireNonNull(a);

 // 다이아몬드 연산자를 이렇게 사용하는 건 자바 9부터 가능하다.
 // 더 낮은 버전을 사용한다면 <Integer>로 수정하자.
 return new AbstractList<Integer>() {
  @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;
  }
 };
}

 메서드가 직접 사용하지 않고 나중에 쓰기위해 저장하는 매개변수는 특히 더 신경 써야 한다. 코드 20-1은 입력받은 int[]의 List뷰를 반환하는 메서드 이다. 이때 Null 검사를 생략하면 새로 생성한 List 인스턴스를 반환하는데 클라이언트가 이를 사용할 때 되서야 NullpointerException을 던지게 되어 디버깅이 힘들어 진다.

생성자는 위 원칙의 특수 사례이다. 이는 클래스 불변식을 어기는 객체를 만들지 않기 위해 꼭 필요하다.

 

물론 예외상황도 있다. 유효성 검사 비용이 지나치게 높거나, 계산 과정에서 암묵적로 될때 생략 가능하다. Collection.sort는 비교 간능한 두 매개 변수를 받아야 하는데 이는 비교하는 과정에서 비교 불가능 하다면 CastException을 던져 예외가 발생한 곳을 바로 알 수 있어 유효성 검사를 생략 가능하다.

 API문서에 던지기로한 예외와 계산중 매개변수 때문에 발생한 예외와 다른 상황이 올 수 있는데, 이 경우 예외 번역 관용구를 사용하여 API에 기재된 예외로 번역해 줘야 한다.

 

결론
 이번 아이템은 '매개변수에 제약을 두어라'가 아니다. 메서드는 항상 범용적이고 제약이 적을수록 좋다.
하지만 제약이 존재해야 한다면 이를 문서화하고 코드 시작 부분에 명시적으로 검사하는 습관을 들이자. 그 노력은 실제 오류를 걸러낼 때 충분히 보상받는다