ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아이템49. 매개변수가 유효한지 검사하라
    책/이펙티브자바 2021. 8. 30. 19:40

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

    예를들어. 인덱스 값은 음수이면 안되며, 객체 참조는 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;
      }
    }
킹수빈닷컴