-
아이템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} ≤ 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; } }
'책 > 이펙티브자바' 카테고리의 다른 글
아이템39. 명명 패턴보다 애너테이션을 사용하라 (0) 2021.08.31 아이템50. 적시에 방어적 복사본을 만들라 (0) 2021.08.30 아이템38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 (0) 2021.08.29 아이템35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) 2021.08.28 아이템34. int 상수 대신 열거 타입을 사용하라 (0) 2021.08.28