Item 49 - 매개변수가 유효한지 검사하라
메서드와 생성자
일반적으로 메서드와 생성자 대부분은 입력 매개변수의 값이 특정 조건을 만족하기를 바란다.
1
2
3
4
5
6
7
8
9
10
@Data
@AllArgsConstructor
public class Order {
private String product; // 제품명
private double amount; // 가격
}
public void processOrder(Order order) {
// 주문 처리 로직
}
주문을 위한 Order 클래스가 있고,
주문 처리 로직이 포함된 processOrder 메서드가 있다고 가정한다.
product가 null 이어선 안된다.amount가 음수여선 안된다.
이런 제약은 반드시 문서화해야 하며 메서드 몸체가 실행되기 전에 검증을 해야 한다.
오류를 즉시 잡아라 🚑
오류는 가능한 한 빨리 발생한 곳에서 잡아야 한다.
그렇지 않으면 해당 오류를 감지하기 더욱 어려워지고, 오류 발생 지점을 찾기 힘들어진다.
따라서 메서드 몸체가 실행되기 전에 매개변수를 확인한다면,
잘못된 값이 넘어왔을 때 깔끔한 방식으로 예외를 던질 수 있다.
매개변수 검증이 제대로 이뤄지지 않을 때 생기는 문제
- 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
- 메서드가 잘 수행되지만 잘못된 결과를 반환한다.
- 메서드는 문제 없이 잘 수행됐지만, 메서드에서 사용한 어떤 객체를 이상한 상태로 만들어 미래의 알 수 없는 시점에 메서드와 관련 없는 오류를 낼 수 있다.
아래 단계의 현상일수록 문제는 더 심각해진다.
1번의 경우, 예외를 던지며 에러로그를 남기게 되지만,
2, 3번의 경우, 실제로 데이터 정합성이 깨진 것을 확인했을 때 인지가 가능하기 때문이다.
이렇게 매개변수 검증에 실패하면 실패 원자성(failure atomicity)을 어기는 결과가 나타난다.
실패 원자성?(item76)
호출된 메서드가 실패하더라도 해당 객체는 호출 전의 상태를 유지해야 한다.
예외의 문서화
- Javadoc의
@throws태그를 이용한다. - 보통
IllegalArgumentException,IndexOutOFBoundsException,NullPointerException중 하나가 될 것이다.
예외 문서화 코드의 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 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.
*
* @param m the modulus.
* @return {@code this mod m}
* @throws ArithmeticException {@code m} ≤ 0
* @see #remainder
*/
public BigInteger mod(BigInteger m) {
if (m.signum <= 0)
throw new ArithmeticException("BigInteger: modulus not positive");
BigInteger result = this.remainder(m);
return (result.signum >= 0 ? result : result.add(m));
}
- 위는
BigInteger.mod메서드로 매개변수 m이 0 이하일 때,ArithmeticException이 발생하는 것을@throws태그로 명시하고 있다.
모든 메서드에 적용되는 문서화를 하고 싶다면 클래스 레벨에서 Javadoc을 작성하라.
1
2
All methods and constructors in this class throw {@code NullPointerException}
when passed a null object reference for any input parameter
- m이 null일 때,
NullPointerException을 던지지만 메서드 주석에 언급이 없다. - 위와 같이, 클래스(BigInteger) 레벨 주석에서 모든 메서드와 생성자가
NullPointerException을 던질 수 있다는 점을 기술했기 때문이다.
null check 🕶️
1
this.strategy = Objects.requireNonNull(strategy, "전략");
- @Nullable이나 비슷한 목적의 애너테이션은 비표준 방식이다.
- Java7에 추가된
java.util.Objects.requireNonNull메서드는 유연하고 편하다.- null check를 수동으로 하지 않아도 된다.
- 예외 메시지를 지정할 수 있다.
- 입력을 그대로 반환하므로, 값을 사용하면서 동시에 null check가 가능하다.
- 반환값은 무시하고 순수한 null 검사 목적으로 어디서든 사용 가능하다.
- Java9에서 Objects의 범위 검사 기능이 추가되었다.
checkFromIndexSize,checkFromToIndex,checkIndex- 예외 메시지 지정이 불가능하고, 리스트와 배열 전용이면서 닫힌 범위는 다루지 못한다.
자바의 assert 기능
https://www.baeldung.com/java-assert
공개되지 않은 메서드라면 패키지 제작자가 메서드가 호출되는 상황을 통제할 수 있다.
즉, 유효한 값만이 메서드에 넘겨진다는 것을 보증할 수 있다는 뜻이다.
다시 말해 public이 아닌 메서드라면 assert 단언문을 통해서 매개변수 유효성을 검증할 수 있다.
1
2
3
4
5
6
private void processOrder(Order order) {
assert order.getProduct() != null;
assert order.getAmount() >= 0;
// 주문처리 로직
}
assert단언문은 자신이 선언한 조건이 무조건 참이어야 다음 로직을 수행한다.- 실패하면,
AssertionError를 던진다. - 보통 개발 중에 테스팅 목적으로 사용된다.
나중에 쓰기 위해 저장하는 매개변수의 유효성 검사
✔️ 입력 받은 int 배열을 List 로 변경해주는 정적 팩터리 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static List<Integer> intArrayAsList(int[] a){
Objects.requireNonNull(a); // null 검사
return new AbstractList<Integer>() {
@Override
public Integer get(int index) {
return a[index]; // 0x124124 + index
}
@Override
public int size() {
return a.length;
}
};
}
Objects.requireNonNull을 이용해 배열의 유효성을 검사하지 않았다면, 클라이언트가 반환된List를 사용하려고 하는 시점에서야NullPointerException을 받게 된다.- 이런 경우, 오류 발생 지점을 추적하기 어려워진다.
✔️ 생성자
- “나중에 쓰려고 저장하는 매개변수의 유효성을 검사하라”는 원칙의 특수한 사례
- 생성자 매개변수의 유효성 검사는 클래스 불변식을 위해 꼭 필요하다.
규칙의 예외
- 유효성 검사 비용이 지나치게 높거나, 실용적이지 않을 때
- 계산 과정에서 암묵적으로 검사가 수행될 때
Collections.sort(List)는 리스트 내 객체들이 모두 상호 비교 될 수 있어야 하며, 정렬 과정에서 이러한 비교가 이뤄진다. 만약 상호 비교될 수 없는 타입의 객체가 들어 있다면ClassCastException을 던질 것이다.- 단, 암묵적 유효성 검사에 너무 의존하면 실패 원자성을 해칠 수 있다.
💡 정리
- 메서드나 생성자를 작성할 때 그 매개변수들에 어떠한 제약이 있을지 생각하자.
- 제약들을 문서화하고, 메서드 코드 시작 부분에서 명시적으로 검사하자.