핵심 요약
유효성 검사는 메소드나 생성자가 예상하는 입력 조건을 충족하는지 검사하는 과정이다. 이를 통해 오류를 되도록 빠르게 처리할 수 있다.
공개 메소드는 매개변수 유효성을 반드시 검사해야 하며, 유효하지않은 매개변수가 발견될 때 적절한 예외를 던져야 한다. 일반적으로 사용되는 예외에는 'IllegalArgumentException', 'IndexOutOfBoundsException', 'NullPointerException' 등이 있다.
비공개 메소드는 assert 문을 사용하여 매개변수 유효성을 검사할 수 있다, 그러나 assert 문은 런타임에 기본적으로 비활성화되므로 주의해서 사용해야 한다.
성능상의 이유로 유효성 검사를 생략하는 경우(유효성 검사 비용이 지나치게 높거나 실용적이지 않을 때, 혹은 게산 과정에서 암묵적으로 검사가 수행될 때)도 있지만, 대체로 유효성 검사는 성능에 큰 영향을 미치지 않는다.
단, 매개변수에 무작정 많은 제약을 걸진 말자. 메소드는 최대한 범용적으로 작성되어야 한다.
유효성 검사의 필요성
메소드나 생성자는 대부분 입력 매개변수의 값이 특정 조건을 만족해야 한다. 이런 제약은 반드시 문서화해야 하며 메소드 몸체가 시작되기 전에 검사해야 한다.
유효성 검사를 하지 않을 경우 발생하는 문제점
1. 메소드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
2. 메소드는 잘 수행되지만 잘못된 결과를 반환할 수 있다.
3. 메소드는 잘 수행되지만 객체가 일관되지 않아 미래에 메소드와는 관련없는 오류가 발생할 수 있다.
뭐, 위 내용을 요약하면, 유효성 검사를 하지 않을 경우 실패 원자성을 어길 수 있다.
❔ 실패 원자성(failure atomicity, 아이템 76)
원자성(Atomicity)은 어떤 작업이나 연산이 중간에 중단되지 않고 완전하게 수행되거나, 아니면 전혀 수행되지 않는 성질을 말한다.
유사하게, 실패 원자성(Failure Atomicity)은 메소드나 연산이 실행 중에 예외가 발생하더라고 해당 객체는 메소드 호출 전 상태를 유지하는 특성을 의미한다. 즉, 연산이 실패하면 아무런 변화도 일어나지 않아야 한다는 원칙이다.
이는 데이터의 일관성을 유지하는 데 매우 중요한 원칙으로, 이를 통해 프로그램의 안정성을 높이고 예기치 않은 결과를 방지할 수 있다.
실패 원자성은 연산 중 예외가 발생하면 해당 예외를 적절히 처리하고, 객체의 상태를 일관성 있게 유지하는 방식으로 지킬 수 있다.
유효성 검사 방법
: 공개 메소드
방법 1. JavaDoc 태그를 사용하여 예외를 문서화한다.(아이템 72)
: public과 protected 메소드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화해야 한다.
보통 'IllegalArgumentException', 'IndexOutOfBoundsException', 'NullPointerException' 등이 있다.
/**
* 이 메서드는 두 정수를 더합니다.
*
* @param a 첫 번째 정수
* @param b 두 번째 정수
* @return 두 정수의 합
* @throws IllegalArgumentException 두 값이 0보다 작을 경우
*/
public int add(int a, int b) {
if (a < 0 || b < 0) {
throw new IllegalArgumentException("Values must be greater than 0");
}
return a + b;
}
방법 2. java.util.Objects.requireNonNull 메소드를 사용한다.
해당 메소드는 자바 7에 추가되었으며, 방법 1보다 간결하며 @Nullable와 달리 Java 표준 라이브러리의 메소드이다. null 여부를 확인하여 null인 경우 'NullPointerException'을 발생시킨다.
this.strategy = Objects.requireNonNull(strategy, "strategy cannot be null");
방법 3. Object 클래스에 범위 검사를 위해 추가된 메소드를 사용한다.
해당 메소드들은 자바 9에 추가되었으며, 범위 검사를 수행하고 유효하지 않은 범위에 대해서는 예외(IndexOutOfBoundsException)를 던진다. 리스트와 배열 전용으로 설계되어 [시작 인덱스, 종료 인덱스) 형태의 범위를 사용한다.
1. checkFromIndexSize(int fromIndex, int size, int length)
- 시작 인덱스와 크기가 유효한지 확인한다. 시작 인덱스가 0 또는 양수이고, 크기가 0 또는 양수이며, (시작 인덱스 + 크기 - 1)이 전체 길이보다 작거나 같은 경우에 유효하다고 판단한다.
2. checkFromToIndex(int fromIndex, int toIndex, int length)
- 시작 인덱스와 종료 인덱스가 유효한 범위인지 확인한다. 시작 인덱스가 0 또는 양수이고, 시작 인덱스가 종료 인덱스보다 작거나 같으며, 종료 인덱스가 전체 길이보다 작거나 같을 때 유효하다고 판단한다.
3. checkIndex(int index, int length)
- 단일 인덱스가 유효한지 확인한다. 인덱스가 0 또는 양수이고, 인덱스가 전체 길이보다 작을 때 유효하다고 판단한다.
유효성 검사 방법
: 비공개 메소드
비공개 메소드(private, package-private(default))는 해당 클래스나 동일한 패키지 내의 다른 클래스에서만 접근이 가능하므로, 패키지 제작자가 메서드가 어떻게 호출되는지 통제할 수 있다.
예를 들어, 다음과 같은 코드를 보자:
public class Example {
public void publicMethod(int value) {
// 유효성 검사
if (value <= 0) {
throw new IllegalArgumentException("Value must be positive");
}
privateMethod(value);
}
private void privateMethod(int value) {
assert value > 0; // 비공개 메소드 내부에서의 assert 사용
// ...
}
}
여기서 publicMethod는 공개 메서드로, 어디서든 호출될 수 있다. 따라서 메서드를 사용하는 쪽이 잘못된 값을 넘길 수도 있기 때문에, 메소드 내부에서 입력 매개변수의 유효성을 검사한다.
반면 privateMethod는 비공개 메서드로, Example 클래스 내부에서만 호출할 수 있다. 따라서 이 메서드가 호출되는 모든 경우를 패키지 제작자가 통제할 수 있다. 위의 예제에서는 publicMethod에서 이미 유효성 검사를 수행하므로, privateMethod에는 잘못된 값이 전달될 가능성이 없다. 그러나 assert를 사용해 추가적으로 검증을 수행하고 있다. 유효하지 않을 경우 AssertionError를 던진다. 이 assert는 보통 개발 과정에서만 활성화되며, 런타임에서는 기본적으로 비활성화되어 성능에 영향을 주지 않는다.
메소드가 직접 사용하지는 않으나 나중에 쓰기 위해 저장하는 매개변수는
특히 더 신경 써서 검사해야 한다.
: ex. 생성자
다음 예시는 Objects.requireNonNull을 이용해 null 검사를 수행하여 null일 경우 NullPointerException을 던진다. 만약 이를 생략할 경우 List를 사용하는 런타임에서야 NullPointerException이 발생한다. 즉, 디버깅이 어려워진다.
static List<Integer> intArrayAsList(int[] array){
Objects.requireNonNull(array);
return new AbstractList<Integer>() {
@Override
public Integer get(final int index) {
return array[index];
}
@Override
public Integer set(final int index, final Integer element) {
int oldValue = array[index];
array[index] = element;
return oldValue;
}
@Override
public int size() {
return array.length;
}
};
}
즉, 생성자 매개변수의 유효성 검사는 클래스 불변식을 어기는 객체가 만들어지지 않게 하는 데 꼭 필요하다.
유효성 검사를 생략하는 경우
: 유효성 검사 비용이 지나치게 높거나 실용적이지 않을 때, 혹은 계산 과정에서 암묵적으로 검사가 수행될 때
예시 1. Collections.sort(List)
만약 상호 비교될 수 없는 타입의 객체가 들어 있다면, 그 객체와 비교할 때 ClassCastException이 발생한다. 따라서 먼저 유효성 검사를 할 필요가 없게 된다.
그러나 암묵적 유효성 검사에 크게 의존할 경우, 실패 원자성을 해칠 수 있으므로 주의해야 한다. (ex. 연산 중간에 예외가 발생해 연산이 중단되고, 그로 인해 객체가 부분적으로 수정된 경우)
예외 번역(exception translate)
유효성 검사가 이루어졌으나, 실제 발생한 예외와 API 문서에서 작성한 예외가 다를 수 있다. 이런 경우에는 예외 번역을 사용하여 API 문서에 기재된 예외로 번역해줘야 한다.
❔ 예외 번역(exception translate, 아이템 73)
예외 번역은 저수준 메소드에서 발생한 예외를 더 높은 수준의 예외로 변환하는 패턴이다. 이는 더 추상화된 수준에서 처리할 수 있도록 예외를 재포장(repackaging)하거나 다른 예외로 바꾸는 것을 포함한다.
이 과정은 예외가 프로그램의 다른 부분에 미치는 영향을 최소화하고, 상위 레벨의 코드에서 예외 처리를 더 쉽게 할 수 있도록 한다. 즉, 저수준의 구현 세부사항을 노출하는 대신에, API를 사용하는 사람들에게 더 높은 수준의 문제를 다루도록 한다.
메소드는 최대한 범용적으로 설계해야 한다.
이번 아이템을 매개변수에 최대한 제약을 두자고 이해하면 안 된다. 매개변수 제약은 적을수록 좋다.
'Language > Java' 카테고리의 다른 글
| [effective java] 아이템 51. 메서드 시그니처를 신중히 설계하라. (0) | 2023.07.17 |
|---|---|
| [effective java] 아이템 50. 적시에 방어적 복사본을 만들라. (0) | 2023.07.17 |
| [effective java] 아이템 48. 스트림 병렬화는 주의해서 적용하라. (0) | 2023.07.03 |
| [effective java] 아이템 47. 반환 타입으로는 스트림보다 컬렉션이 낮다. (0) | 2023.07.03 |
| [effective java] 아이템 46. 스트림에서는 부작용 없는 함수를 사용하라. (0) | 2023.07.03 |