49. 매개변수가 유효한지 검사하라

매개변수가 유효한지 검사하라

메서드와 생성자가 기대하는 제약사항을 문서화하고 명시적으로 검사하기

매개변수 검사가 중요한 이유

  • 메서드 본문 실행 전에 오류를 빠르게 감지할 수 있다

  • 오류 원인과 위치를 정확히 파악할 수 있다

  • 실패 원자성(failure atomicity)을 지킬 수 있다

검사하지 않으면 발생할 수 있는 문제

  • 메서드 중간에 모호한 예외가 발생한다

  • 메서드가 잘못된 결과를 반환할 수 있다

  • 객체가 불안정한 상태가 되어 미래에 예상치 못한 오류가 발생할 수 있다

매개변수 검사 방법

일반적인 검사 방법

/**
 * (현재 값 mod m) 값을 반환한다.
 * 항상 음이 아닌 BigInteger를 반환한다는 점에서 remainder와 다르다.
 *
 * @param m 계수(양수여야 함)
 * @return 현재 값 mod m
 * @throws ArithmeticException m이 0 이하일 경우
 */
public BigInteger mod(BigInteger m) {
    if (m.signum() <= 0)
        throw new ArithmeticException("계수(m)는 양수여야 합니다: " + m);

    return ... // 계산 수행
}

Java 7부터 도입된 유틸리티 메서드

// Objects.requireNonNull - null 검사
this.strategy = Objects.requireNonNull(strategy, "전략이 null일 수 없습니다");

// Java 9: requireNonNullElse - null일 경우 기본값 제공
this.timeout = Objects.requireNonNullElse(timeout, DEFAULT_TIMEOUT);

// Java 9: 인덱스 검사 유틸리티
this.subList = Objects.checkFromIndexSize(fromIndex, size, list.length);
this.subList = Objects.checkFromToIndex(fromIndex, toIndex, list.length);
this.element = Objects.checkIndex(index, list.length);

private 메서드에서는 assert 활용

private static void sort(int[] a, int fromIndex, int toIndex) {
    assert fromIndex >= 0;
    assert toIndex <= a.length;
    assert fromIndex <= toIndex;

    // 정렬 로직...
}

✔️ 특징

  • assert는 개발 중에만 활성화된다 (-ea 또는 --enableassertions 옵션 필요)

  • 런타임에는 성능 저하가 없다

  • private 메서드는 호출되는 상황을 통제할 수 있으므로 assert로 충분하다

특별한 상황들

📌 생성자 매개변수 검사

public Period(Date start, Date end) {
    // null 검사와 방어적 복사를 함께 수행
    this.start = new Date(Objects.requireNonNull(start, "시작 날짜가 null일 수 없습니다").getTime());
    this.end = new Date(Objects.requireNonNull(end, "종료 날짜가 null일 수 없습니다").getTime());

    // 날짜 관계 검증
    if (this.end.before(this.start)) {
        throw new IllegalArgumentException(
            "종료 날짜가 시작 날짜보다 이전일 수 없습니다");
    }
}

✔️ 중요성

  • 생성자는 객체의 불변식을 보장하는 첫 번째 방어선이다

  • 잘못된 객체가 생성되는 것을 막을 수 있다

📌 필드로 저장하는 매개변수

public void setBackgroundColor(Color color) {
    this.backgroundColor = Objects.requireNonNull(color);
}

✔️ 특징

  • 나중에 사용하려고 저장하는 매개변수는 특히 더 신경 써서 검사해야 한다

  • null이 저장되면 나중에 NullPointerException이 발생할 수 있다

📌 고비용 검사와 암묵적 검사

고비용 검사

// 큰 배열이 정렬되어 있는지 검사하는 것은 비용이 높음
public void processSortedArray(int[] hugeArray) {
    // 정렬 검사를 하지 않음 (문서화로 대체)
}

암묵적 검사

// Collections.sort는 내부적으로 타입 검사를 수행
Collections.sort(list);

✔️ 주의사항

  • 예외적인 상황에서만 검사를 생략한다

  • 문서화를 통해 제약조건을 명확히 한다

  • 암묵적 검사에 과도하게 의존하면 실패 원자성이 깨질 수 있다

실패 원자성 보장하기

public void transfer(Account to, double amount) {
    // 모든 검사를 먼저 수행
    Objects.requireNonNull(to, "대상 계좌가 null일 수 없습니다");

    if (amount <= 0)
        throw new IllegalArgumentException("양수 금액이어야 합니다: " + amount);

    if (amount > balance)
        throw new IllegalArgumentException("잔액 부족");

    // 모든 검사 통과 후 상태 변경
    balance -= amount;
    to.balance += amount;
}

✔️ 실패 원자성이란?

  • 메서드 호출이 실패하더라도 객체는 호출 전 상태를 유지해야 함

  • 매개변수 검사는 상태 변경 전에 수행해야 함

표준 예외 사용하기

예외 타입
사용 시기

IllegalArgumentException

인수 값이 부적절함

IndexOutOfBoundsException

인덱스가 범위를 벗어남

NullPointerException

null 값이 전달됨

IllegalStateException

객체 상태가 메서드 호출에 부적절함

ConcurrentModificationException

허용하지 않는 동시 수정 감지

UnsupportedOperationException

호출한 메서드를 지원하지 않음


🧩 어려웠던 점

  • 너무 엄격하면 유연성이 떨어지고, 너무 느슨하면 안정성이 떨어진다

  • 성능이 중요한 상황에서 매개변수 검사와 성능 사이의 균형을 잡는 것

  • 여러 매개변수 간의 복잡한 관계를 검증하는 코드를 깔끔하게 작성하는 것

💭 느낀 점

  • 매개변수 검사는 API의 사용성을 높이고 오류 메시지를 명확하게 해준다

  • 제약조건을 명시적으로 검사하는 것은 코드의 의도를 분명히 드러내준다

  • 유틸리티 메서드(Objects.requireNonNull 등)를 활용하면 코드가 더 간결해진다

Last updated