Effective Java : 아이템 49. 매개변수가 유효한지 검사하라.

    Effective Java : 아이템 49. 매개변수가 유효한지 검사하라. 

    • 메서드가 시작할 때 매개변수 유효성 검사를 해라. 만약 여기서 오류를 잡지 못하면, 메서드 내에서 알 수 없는 에러가 발생하거나 이상한 객체가 만들어져 다른 메서드에 예외를 발생시킬 수 있다. 
    • 공개 API는 매개변수 유효성 검사에 실패했을 때 발생할 수 있는 에러와 조건을 문서화 해야함. (@throws 태그)
    • private API에는 반드시 유효한 값만 전달되도록 개발자가 직접 작성해야 함. 이것을 전제로 assert 문으로 처리할 수 있음.
      • assert 문은 일반적으로는 런타임에서 무시됨. (성능 영향 없음)
      • 필요한 경우 java -ea로 활성화 할 수 있음.
    • 나중에 사용할 값을 매개변수로 받는 경우, 매개변수 유효성 검사를 가급적이면 해주자. 이상한 곳에서 에러가 발생해 추적하기 어려울 수 있다. 
    • 매개변수 유효성 검사의 비용이 크거나, 계산에서 암묵적 유효성 검사가 진행되는 경우 유효성 검사를 스킵할 수도 있음. 
      • 암묵적 유효성 검사에 너무 의존하는 경우 실패 원자성이 깨짐. 
    • 유효성 검사에서 의도치 않은 에러를 던질 수도 있음. 이 부분은 예외 번역(Exception translate) 관용구를 사용하여 API 문서에 기재된 예외로 번역해줘야 함. 

     

     


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

    메서드가 시작되는 지점에서 매개변수가 전제조건을 만족하는지 먼저 검사해야한다. 예를 들면 null이 아니거나, 양수이거나 같은 조건들이다. 만약 매개변수의 오류를 시작지점부터 잡지 못하면, 이 오류가 이후에 프로그램에 어떤 결과를 가져올지 알 수 없다. 더 나아가서 언젠가 오류가 발생했을 때, 어디서부터 문제가 발생했는지 알 수 없다.

    • 메서드가 수행되는 중간에 알지 못하는 예외를 던지며 실패할 수 있음. 
    • 메서드가 잘 수행되었지만 의도한 것과 다른 객체를 만든다. 
    • A 메서드는 문제 없이 수행되었으나, 의도한 것과 다른 객체를 만든다. 그리고 이 객체를 다른 메서드 B가 사용할 때, 오류가 발생한다. 이 때, B 메서드에서 발생한 오류는 메서드 A와 관련이 없어 보인다. 

    마지막 2개의 내용은 '실패 원자성'과 관련된 이야기다. 아래 코드가 예시다.

    • NullPointer 클래스는 반드시 myObject를 가질 것을 기대함. 
    • myString 매개변수에 null값이 전달되어도 create() 메서드는 정상적으로 수행됨. 의도치 않은 NullPointer 객체가 생성됨.
    • print() 메서드에서 nullPointer.getMyObject()를 호출했을 때 NPE가 발생함. 
    public class ShouldCatchExceptionBefore {
    
        @Getter
        @RequiredArgsConstructor 
        static class NullPointer {private final String myObject;}
        
        public NullPointer doSomething(String myString) {
            NullPointer nullPointer = this.create(myString);
            print(nullPointer);
            return nullPointer;
        }
    
        private NullPointer create(String myString) {
            return new NullPointer(myString);
        }
    
        private void print(NullPointer nullPointer) {
            System.out.println("nullPointer.myString = " + nullPointer.getMyObject());
        }
    }

     


    public / protected 메서드는 매개변수 값이 잘못되었을 때 던지는 예외를 문서화 해야함. 

    public, protected는 공개된 API이기 때문에 다양한 사람들이 사용할 수 있다. 이런 공개 API에서는 매개변수의 값이 잘못되었을 때 던지는 예외를 문서화 해야한다. 자바의 BigInteger 클래스의 mod() 메서드를 살펴보자. 

    위 코드는 다음 관점에서 잘 되어 있다. 

    1. 시작할 때, 매개변수 유효성 검사를 진행함. (m.signum <= 0)
    2. 발행할 수 있는 에러를 @throws로 표현하고, 이 때 에러가 발생하는 매개변수의 전제조건 (m <=0)을 보여줌. 

    만약 오픈소스로 공개된 API를 구현한다면 위와 같이 구현하도록 해야한다. 

     


    private API는 assert 문을 사용하자. 

    private API는 직접 개발하는 메서드다. private API를 개발할 때는 유효한 값만이 메서드에 넘겨지도록 해야만 한다. 이것을 전제로 한다면 유효성 검사를 'assert'문으로 대신할 수 있다. 

    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;
    }

    위 코드에서 볼 수 있듯이 비공개 API를 개발할 때, '반드시 유효한 값만 전달되도록' 설계하면 assert 문으로 전제조건을 '표시'할 수 있다. 코드 내에 전제조건이 표시되기 때문에 문서를 읽지 않아도 된다는 장점이 있으며, 런타임 환경에서는 일반적으로 영향을 주지 않기 때문에 없는 코드로 볼 수도 있다. 

     


    메서드가 직접 쓰지 않으나, 나중에 사용하려 저장하는 매개변수는 신경써서 검사해야 함. 

    메서드에 전달되는 매개변수지만, 지금 당장 쓰지 않고 나중에 사용하기 위해 저장하는 매개변수 같은 것들이 있다. 이런 매개변수들은 '사용되는 시점'에 문제가 발생할 수 있기 때문에 디버깅에 많은 비용이 발생할 수 있다. 따라서 이런 변수들은 조금 더 신경써서 검사를 해야한다. 

    public class ShouldCatchExceptionBefore {
    
        @Getter
        @RequiredArgsConstructor 
        static class NullPointer {private final String myObject;}
        
        public NullPointer doSomething(String myString) {
            NullPointer nullPointer = this.create(myString);
            print(nullPointer);
            return nullPointer;
        }
    
        private NullPointer create(String myString) {
            return new NullPointer(myString);
        }
    
        private void print(NullPointer nullPointer) {
            System.out.println("nullPointer.myString = " + nullPointer.getMyObject());
        }
    }

    위 같은 코드가 문제가 된다. null이 넘겨지더라도 create() 메서드는 정상적으로 수행되어서 원치 않는 이상한 객체를 만들어서 반환한다. 그리고 이 객체가 사용되는 시점에 에러가 발생한다. '나중에 쓰기 위해서' 담아두는 것들은 필드가 될 수도 있을 것이고, List 형태가 될 수도 있을 것이다. 

    private NullPointer create(String myString) {
        assert myString != null;
        return new NullPointer(myString);
    }

    이런 일들이 발생하지 않도록 나중에 사용하는 필드 / Collection 타입일 경우 매개변수 검사를 더 철저히 해주는 것이 좋다.

     


    메서드 실행 전, 매개변수 유효성 검사의 예외

    가급적이면 메서드 실행 전 매개변수 유효성 검사를 하는 것이 좋으나 예외 경우가 두 가지 존재한다.

    • 유효성 검사 비용이 너무 큰 경우.
    • 계산 과정에서 암묵적으로 유효성 검사가 진행되는 경우.  (이 경우는 전제조건에서 검사해도 실익이 별로 없음)
      • 너무 의존할 경우, 실패 원자성이 깨짐.

    위 경우에는 메서드 실행 전 유효성 검사를 진행하지 않아도 된다. 계산 과정에서 암묵적 유효성 검사가 일어나는 것은 다음 코드로 이해할 수 있다.

    Collections.sort(myList);

    정렬을 수행할 때, '암묵적 유효성 검사'가 발생한다. List 내부 원소들이 서로 Compartor가 가능한지를 확인한 후에, 비교 연산을 진행한다. 이 때 Comparator가 가능한지를 확인하는 과정이 '암묵적 유효성 검사'다. 어차피 이런 암묵적 유효성 검사가 이루어지고, 실패하면 ClassCastException이 알려주기 때문에 굳이 전제조건에서 List 전체에 대한 유효성 검사를 할 필요가 없다는 것이다.

    한 가지 단점은 이런 암묵적 유효성 검사에 너무 의존하는 경우 '실패 원자성' 관점에서는 좋지 않을 수 있다는 것이다. 예를 들어 아래 코드에서는 메서드 실행 도중에 에러가 발생한다. 

    public static void main(String[] args) {
    
        List<Integer> integerList = new ArrayList<>(List.of(1,2,3,4,5,6,7));
        try {
            update(integerList);
        } catch (Exception e) {
            System.out.println(integerList);
        }
    
    
    }
    
    public static void update(List<Integer> integers) {
        for (int i = 0; i < integers.size(); i++) {
            Integer integer = integers.get(0);
            int newInteger = integer + 10;
            if (i == 1) {
                throw new RuntimeException();
            }
            integers.add(i, newInteger);
        }
    }

    그런데 만약 발생한 예외를 잡아서 계속 쓰는 상황을 가정해본다면, 메서드가 실패했음에도 불구하고 integerList의 값은 아래에서 볼 수 있듯이 일부 변했을 수도 있다. 이런 것이 실패 원자성이 깨졌다는 것을 의미한다. 

    [11, 1, 2, 3, 4, 5, 6, 7]

    암묵적 유효성 검사에만 너무 의존하게 되면, 언제 예외가 발생할지 알 수 없으며 예외가 발생했을 때 실패 원자성이 보장 되는지도 알 수 없다. 

     


    유효성 검사에서 의도치 않은 에러가 던져질 수 있음. 

    유효성 검사를 하던 도중 의도치 않은 에러가 발생할 수도 있다. 예를 들어 아래 코드에서는 num에 0이라는 값이 들어오면 '0'으로 나눴기 때문에 RuntimeException이 아닌 ArithmeticException이 발생할 수 있다.

    유효성 검사 실패 시 던지기로 한 에러와 실제로 던져지는 에러가 다른 것인데, 이런 경우 문서에 예외 번역 관용구를 추가해서 처리해볼 수 있다. 혹은 아이템 73 (추상화 수준에 맞는 예외를 던져라)를 적용해 볼 수 있다.

    private static final int initValue = 10;
    
    
    public void testMethod(int num) {
        if ((initValue / num > 0) && num > 0) {
            throw new RuntimeException();
        }
    }

     

    댓글

    Designed by JB FACTORY