티스토리 뷰

메소드와 생성자 대부분은 입력 매개변수의 값이 특정 조건을 만족하길 바란다.

예를들어. 인덱스 값은 음수이면 안되며, 객체 참조는 Null이 아니길 기대한다.

 

이 책에서 계속 꾸준히 하는 말인데 "오류는 가능한 한 빨리 잡아야 한다".

말하자면 메서드 body가 시작하는 부분에서 먼저 검사하라 이 말이다.

검사를 제대로 하지 않는다면 1. 메서드 수행 중간에 모호한 예외를 던질 수 있고, 2. 메서드가 잘 수행되지만 잘못된 결과를 반환할수 있고, 3. 문제없이 수행 되지만 어떤 객체를 이상한 상태로 만들어 놓아 미래에 알 수 없는 시점에 메서드와 관련없는 오류를 낼때다.

즉, 매개변수 검사에 실패하면 실패 원자성을 어기는 결과를 낳을 수 있다.


public, protected 메서드는 (접근이 쉬운 편) 매개변수 값이 잘못됐을 때 던지는 예외를 문서화 해야한다.

매개변수 제약을 문서화한다면 그 제약을 어겼을 때 발생하는 예외도 함께 기술해야 한다. (@throws)

ex. BigInteger.mod()

/**
 * 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.
 *
 *@paramm the modulus.
 *@return {@codethis mod m}
 *@throws ArithmeticException{@codem} &le; 0
 *@see #remainder
*/
public BigInteger mod(BigInteger m) {
    if(m.signum <= 0)
        throw newArithmeticException("BigInteger: modulus not positive");
  BigInteger result =this.remainder(m);
    return(result.signum >= 0 ? result : result.add(m));
}

여기 예시에서 m이 null이면 NullPointerException을 던진다는 말이 없는데 그 이유는 이 설명을 개별 메서드 부분이 아니라 BigInteger 클래스 수준에서 기술했기 때문이다.

// BigInteger의 class Doc
All methods and constructors in this class throw NullPointerException when passed a null object reference for any input parameter.

클래스 수준 주석은 그 클래스의 모든 public 메서드에 적용되므로 각 메서드에 일일이 기술하는 것 보다 깔끔하다.


자바7에 추가된 java.util.Objects.requireNonNull() 은 유연하고 사용하기도 편하니, 더 이상 null 검사를 수동으로 하지 않아도 된다.

Objects.requireNonNull()

안에 코드를 보면 생각보다 간단하다. 그냥 null 검사 이후 null이면 NPE 던지고 아니면 obj 을 리턴한다.

// 1
public static <T> T requireNonNull(T obj) {
  if (obj == null)
    throw new NullPointerException();
  return obj;
}

// 2. 메세지 자체를 전달
public static <T> T requireNonNull(T obj, String message) {
  if (obj == null)
    throw new NullPointerException(message);
  return obj;
}

// 3. 메세지를 반환하는 Supplier 객체를 전달받아 lazy 하게 작동한다.
public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier) {
  if (obj == null)
    throw new NullPointerException(messageSupplier == null ?
       null : messageSupplier.get());
  return obj;
}

코드가 깔끔해질 뿐더러 생성 시점에 바로 NPE 를 던지는 장점이있다.

최대한 빨리 오류를 잡아라 라는 말에 맞는다.

public class Item49_2 {

  // 왜 Objects.requireNoNull 을 사용해야 하는가 ?
  public static void main(String[] args) {
    // 생성 시점에 바로 NullPointerException 발생
    A a = null;
    B b = new B(a);

    // 사용 시점에 NPE 발생
    C c = new C(a);
    System.out.println("-------");
    String test = c.getA().name;
  }
}

// 
class A {
  String name;
}

class B {
  private final A a;

  public B(A a) {
    this.a = Objects.requireNonNull(a);
  }

  public A getA() {
    return a;
  }
}

class C {
  private final A a;

  public C(A a) {
    this.a = a;
  }

  public A getA() {
    return a;
  }
}

 


Objects.requireNonNull 은 자바 7에 추가됐는데 자바 9에서 아래 메서드들이 추가됐다.

checkFromIndexSize(), checkFromToIndex(), checkIndex(), requireNonNullElse(), requireNonNullElseGet()

Objects.requireNonNullElse(), requireNonNullElseGet()

public static <T> T requireNonNullElse(T obj, T defaultObj) {
  return (obj != null) ? obj : requireNonNull(defaultObj, "defaultObj");
}

public static <T> T requireNonNullElseGet(T obj, Supplier<? extends T> supplier) {
  return (obj != null) ? obj
  : requireNonNull(requireNonNull(supplier, "supplier").get(), "supplier.get()");
}

Objects.checkFromIndexSize(), checkFromToIndex(), checkIndex()

public static int checkFromIndexSize(int fromIndex, int size, int length) {
  return Preconditions.checkFromIndexSize(fromIndex, size, length, null);
}

public static int checkFromToIndex(int fromIndex, int toIndex, int length) {
  return Preconditions.checkFromToIndex(fromIndex, toIndex, length, null);
}

public static int checkIndex(int index, int length) {
  return Preconditions.checkIndex(index, length, null);
}

null 검사 메서드만큼 유연하지는 않다.

예외메시지 지정 할 수 없고, 리스트와 배열 전용으로 설계되었다.

닫힌 범위 (양 끝단 값을 포함하는)는 다루지 못한다.

이런 제약이 걸림돌이 되지 않는 상황에서는 유용하고 편하다.

assert 문

  • public이 아닌 메서드라면 단언문(assert)를 사용해 매개변수 유효성을 검증할 수 있다.
  • private 은 객체 스스로가 호출할 것임이 명확하기 때문이다.
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;

    ...
}

assert [Boolean] // 참이면 Pass, 거짓이면 AssertionError
  • jdk 1.4부터 지원
  • JVM은 기본적으로 assertion 유효성 검사를 비활성화
    • 이전 버전의 호환성 유지하기 위해 (이전에는 예약어가 아니었음)
  • 옵션을 통해 유효성 검사 가능
    • 포함할경우: -ea
    • 배제할경우: -da

  • 생성자 매개변수의 유효성 검사는 클래스 불변식을 어기는 객체가 만들어지지 않게 하는데 꼭 필요하다
  • 앞에서 메서드 바디 실행 전에 유효성 검사를 실시하라고 하였는데 이 규칙에도 예외는 있다.
    • 유효성 검사 비용이 지나치게 높거나 실용적이지 않을때
    • 계산 과정에서 암묵적으로 검사가 수행될 때
      • ex.Collections.sort(List)
      • 리스트 안의 객체들은 모두 상호 비교될 수 있어야 하는데 만약 상호 비교될 수 없는 타입의 객체가 들어 있다면 객체 비교시 ClassCastException 을 던질것이다.
      • 비교하기 앞서 리스트 안의 모든 객체가 상호 비교될 수 있는지 검사해봐야 실익이 없다.

결론

  • 메서드나 생성자를 작성할 때면 그 매개변수들에 어떤 제약이 있을지 생각해야 한다.
  • 그 제약들을 문서화 하고 메서드 코드 시작 부분에서 명시적으로 검사해야 한다.

Objecs 메소드들 테스트 해보기

public class Item49Tests {

  private A createNonNullA() {
    return new A("x");
  }

  private A createNullA() {
    return null;
  }

  @DisplayName("requireNonNull - not null 일때 통과한다.")
  @Test
  void testRequireNonNull1() {
    A a = createNonNullA();

    Objects.requireNonNull(a);

    assertDoesNotThrow(() -> (Objects.requireNonNull(a)));
  }

  @DisplayName("requireNonNull - null 일때 빈 메시지의 NPE 를 던진다.")
  @Test
  void testRequireNonNull2() {
    A a = createNullA();

    assertThrows(NullPointerException.class,
            () -> Objects.requireNonNull(a));
  }

  @DisplayName("requireNonNull - null 일때 2번 매개변수 메시지의 NPE 를 던진다.")
  @Test
  void testRequireNonNull3() {
    A a = createNullA();

    Exception exception = assertThrows(NullPointerException.class,
            () -> Objects.requireNonNull(a, "For input A instance"));

    String expectedMessage = "For input A instance";
    String actualMessage = exception.getMessage();

    assertTrue(actualMessage.contains(expectedMessage));
  }

  @DisplayName("requireNonNull - null 일때 Supplier<String> 메시지를 NPE 에 담아 를 던진다.")
  @Test
  void testRequireNonNull4() {
    A a = createNullA();

    Exception exception = assertThrows(NullPointerException.class,
            () -> Objects.requireNonNull(a, () -> ("hello")));

    String expectedMessage = "hello";
    String actualMessage = exception.getMessage();

    assertTrue(actualMessage.contains(expectedMessage));
  }

  @DisplayName("requireNonNullElse - 둘 다 not null 일때 1번 매개변수를 리턴한다.")
  @Test
  void testRequireNonNullElse1() {
    A a1 = new A("x");
    A a2 = new A("y");

    A actualA = Objects.requireNonNullElse(a1, a2);

    assertEquals(a1, actualA);
  }

  @DisplayName("requireNonNullElse - 1번 매개변수가 null 일때 2번 매개변수를 리턴한다.")
  @Test
  void testRequireNonNullElse2() {
    A a1 = createNullA();
    A a2 = new A("y");

    A actualA = Objects.requireNonNullElse(a1, a2);

    assertEquals(a2, actualA);
  }

  @DisplayName("requireNonNullElse - 둘 다 null 일때 NPE 를 던진다.")
  @Test
  void testRequireNonNullElse3() {
    A a1 = createNullA();
    A a2 = createNullA();

    assertThrows(NullPointerException.class,
            () -> Objects.requireNonNullElse(a1, a2));
  }

  @DisplayName("requireNonNullElseGet - 1번 매개변수가 null이면 Supplier 를 리턴한다.")
  @Test
  void testRequireNonNullElseGet() {
    String expected = "x";

    String actual = Objects.requireNonNullElseGet(null, () -> "x");
    assertEquals(expected, actual);
  }

  // ----------------------------------------------------------------
  @DisplayName("checkIndex - 0 <= index < length 일때 index 반환한다.")
  @Test
  void testCheckIndex1() {
    int index = 4;
    int length = 5;
    assertEquals(index, Objects.checkIndex(index, length));
  }

  @DisplayName("checkIndex - index >= length 일때 IndexOutOfBoundsException 던진다.")
  @Test
  void testCheckIndex2() {
    int index = 5;
    int length = 3;
    assertThrows(IndexOutOfBoundsException.class,
            () -> Objects.checkIndex(index, length));
  }

  @DisplayName("checkFromToIndex - from ~ to 가 0 ~ length 사이에 있으면 fromIndex 를 리턴한다.")
  @Test
  void testCheckFromToIndex1() {
    int fromIndex = 0;
    int toIndex = 3;
    int length = 3;
    assertEquals(fromIndex, Objects.checkFromToIndex(fromIndex, toIndex, length));
  }

  @DisplayName("checkFromToIndex - from ~ to 가 0 ~ length 사이에 없으면 IndexOutOfBoundsException 를 던진다.")
  @Test
  void testCheckFromToIndex2() {
    int fromIndex = 0;
    int toIndex = 5;
    int length = 3;
    assertThrows(IndexOutOfBoundsException.class,
            () -> Objects.checkFromToIndex(fromIndex, toIndex, length));
  }

  @DisplayName("checkFromIndexSize - from ~ from+size 가 0~length 사이에 있으면 from을 리턴한다.")
  @Test
  void checkFromIndexSize1() {
    int length = 5;
    assertEquals(3, Objects.checkFromIndexSize(3, 2, length));
  }

  @DisplayName("checkFromIndexSize - from ~ from+size가 0~length 사이에 없으면 IndexOutOfBoundsException 를 던진다.")
  @Test
  void checkFromIndexSize2() {
    int length = 5;
    assertThrows(IndexOutOfBoundsException.class,
            () -> Objects.checkFromIndexSize(3, 3, length));
  }
}

class A {
  private final String x;

  public A(String x) {
    this.x = x;
  }

  public String getX() {
    return x;
  }
}