Effective Java : 아이템 71. 필요없는 체크 예외 사용은 피하라

    아이템 71. 필요없는 체크 예외 사용은 피하라

    • 체크 예외는 복구 가능한 상황을 알려서 이 API를 사용하는 개발자가 복구를 시도하도록 강제함. 
    • 복구 불가능한 예외라면 비검사 예외를 던져서 사용하기 편리한 API를 제공 해야 함.
    • 복구 가능한지 명확하지 않다면 비검사 예외를 던져라.
    • 검사 예외가 추가되면 다음 문제가 발생함.
      • try ~ catch를 사용해야 함. 
      • Stream에서 사용할 수 없음. 
    • 검사 예외를 회피하는 방법
      • Optional 반환하기 → 단점은 예외 정보가 없음.
      • 검사 예외를 던지는 메서드를 2개로 쪼개서, 상태 검사로 분기 처리하기 → 멀티 쓰레드 환경에서 상태검사 동기화가 필요함. 

     


    체크 예외를 던지면 사용하기 어려운 API가 됨. 

    메서드에서 체크 예외를 던지면, 이 메서드를 사용하는 클라이언트는 반드시 이 예외를 처리하는 코드를 작성해야한다. 체크 예외는 다음 두 가지를 강제하는데, 이런 이유 때문에 사용하기 어려운 메서드가 된다. 

    • Try ~ Catch 절을 언젠가는 반드시 넣어야 함.  (예외를 상위 메서드로 던지더라도)
    • Stream에서 사용할 수 없음. 

    이미 Try ~ Catch 절을 사용하고 있는 경우라면 체크 예외가 하나 추가되었을 때 Catch 절을 하나 추가하기만 하면 되지만, 그렇지 않은 경우라면 Try ~ Catch 절을 최초로 사용해야한다. 그래서 좀 더 부담이 큰 작업이 될 수 있다. 

    아무튼 체크 예외를 던지면 사용하기 어려운 API가 되기 때문에 필요없는 경우에 체크 예외를 남발하는 것은 오히려 문제가 된다. 따라서 정말로 체크 예외를 던지는게 필요한지를 고민하고 체크 예외를 던지도록 해야한다. 


    체크 예외를 회피하는 방법

    어쩔 수 없이 체크 예외를 던져야하는 메서드 일 때, 사용 편의성을 위해서 체크 예외를 던지지 않도록 바꾸려면 다음 두 가지 방법을 고민해 볼 수 있다. 

    • 체크 예외 대신 Optional 객체 반환하기
    • 체크 예외를 던지는 메서드 1개 →  2개의 메서드로 분리 후, 상태 검사로 조건분기 처리하기

    첫번째 방법은 체크 예외를 던지는 대신 빈 Optional 객체를 반환하는 것이다. 이 방식의 단점은 Optional에 기존 에러 정보가 포함될 수 없다는 점이다. 따라서 예외가 발생했을 때, 구체적인 예외 타입과 예외 객체가 제공하는 메서드를 활용할 수 없다는 것이 단점이다

    // 체크 예외 하나만 발생하는 경우. 
    try {
    	obj.action(args);
    } catch (TheCheckedException e) {
    	// 예외 상황에 대처한다.
    }
    
    // 두 개로 분리한 경우
    
    if (obj.permittedArgs(args)) {
    	obj.action(args);
    } else { 
    	// 예외 상황에 대처한다.
    }

    편의성을 위해 체크 예외를 우회하는 또 다른 방법은 체크 예외를 던지는 메서드 1개를 2개의 메서드로 분리 후, 체크 예외 부분을 상태 검사로 바꾸는 방법이다.

    • obj.action() 메서드는 체크 예외를 던졌었음. 
    • 예외 상황 확인을 위해 permittedArgs()로 분리함. 예외 상황을 던져야 한다면 false를 반환하도록 하고, 그 경우에 대한 대응을 else 문에서 처리함.

    이 방법 역시 단점이 있는데, '상태 검사'에 의존적이기 때문에 멀티 쓰레드 동기화가 되지 않는 상황이라면 문제가 될 수 있다. 쓰레드마다 하나의 객체를 가지고 있다면 이런 것들을 고민할 필요가 없다. 하지만 많은 쓰레드가 공유해서 사용하고 있는 객체라면 상태 검사 → 메서드 실행 하는 단계에서 상태 값이 바뀌면서 문제가 될 수 있다. 

    한 가지 구체적인 예시로는 이런 방법이 될 수 있을 것이다.

    public class ThrowCheckException {
    
        static class CheckedException extends Exception {
        }
        
        public void action(String arg) throws CheckedException {
            if (arg == null) {
                throw new CheckedException();
            }
            System.out.println(arg);
        }
    
        public static void main(String[] args) {
    
            ThrowCheckException throwCheckException = new ThrowCheckException();
            try {
                throwCheckException.action(null);
            } catch (CheckedException e) {
                e.printStackTrace();
            }
        }
    }

    위에서 action() 메서드는 매개변수가 null인 경우 CheckedException()을 던진다. main()에서 action()을 호출했을 때, CheckedException이기 때문에 try ~ catch를 이용해서 처리해야했었다. 그런데 굳이 CheckedException을 던지지 않고 싶다면, 아래처럼 메서드를 분리할 수도 있다.

    public class ThrowCheckExceptionRefactor {
    
        static class CheckedException extends Exception {}
        
        public boolean canAction(String arg) {
            return arg == null;
        }
        
        public void action(String arg){
            System.out.println(arg);
        }
    
        public static void main(String[] args) {
            ThrowCheckExceptionRefactor throwCheckExceptionRefactor = new ThrowCheckExceptionRefactor();
            String arg = "abc";
            if (throwCheckExceptionRefactor.canAction(arg)) {
                throwCheckExceptionRefactor.action(arg);
            } else {
                System.out.println("error 발생");
            }
        }
    }

    아래에서는 예외가 발생해야하는 상황인지 확인(전제 조건 확인)을 한다. 만약 예외가 발생해야 할 상황이라면 false를 반환하고, else 조건으로 넘어가게 된다. else 절에서는 발생했어야 할 체크 예외를 처리하는 로직을 작성하면 깔끔하게 해결해 볼 수도 있다.

    댓글

    Designed by JB FACTORY