Effective Java : 아이템 73. 추상화 수준에 맞는 예외를 던져라.

    Effective Java : 아이템 73. 추상화 수준에 맞는 예외를 던져라.

    • 저수준 예외는 고수준 예외로 변경(예외번역)해서 상위 계층으로 던져야 함. 현재 계층의 저수준 예외가 상위 계층으로 던져지면, 현재 계층의 내부 구현이 상위 계층을 오염시킴. 
    • 예외 번역 시, 저수준 예외가 디버깅에 도움이 된다면 예외 연쇄를 사용. 

     


    저수준 예외는 고수준 예외로 변경

    저수준 예외를 고수준으로 추상화해서 상위 계층으로 던져야 한다. 그렇지 않으면 하위 계층의 내부 구현을 상위 계층이 알게 된다. 이 말은 상위 계층이 하위 계층의 내부 구현을 알고 적절히 대응해야 하는 것을 의미한다. 클래스 간의 의존성이 높아지는 방향이기 때문에 지양해야 한다.  일반적으로 예외 번역은 이런 식으로 사용할 수 있다. 

    // 하위 계층
    try { 
    	...
    } cathc (LowerLevelException e) {
      throw new HigherLevelException();
    }

    한 가지 적절한 예시로는 Spring의 Service - Repository 계층을 들 수 있다.  Spring의 @Repository을 가진 클래스는 DB와 관련된 에러가 발생하더라도 스프링 예외 번역기가 적절한 형태의 추상화 된 예외로 바꾸어 던져준다. 따라서 서비스 계층은 리포지토리 계층이 어떤 데이터 접근 계층을 사용하더라도 그곳에 분리되어서 처리하기만 하면 된다는 것이다. 

    public class BadExceptionTranslator {
    
        @RequiredArgsConstructor
        static class BadHighClass {
            private final BadLowClass lowClass;
            public void doSomething(){
                try {
                    lowClass.throwError();
                } catch (IndexOutOfBoundsException e) {
                    System.out.println();
                }
            }
        }
    
        static interface BadLowClass {
            void throwError();
        }
    
        static class BadLowClassV2 implements BadLowClass {
            @Override
            public void throwError() {
                throw new AcceptPendingException();
            }
        }
    
        static class BadLowClassV1 implements BadLowClass {
            @Override
            public void throwError(){
                throw new IndexOutOfBoundsException();
            }
        }
    }

    위 코드는 가내 수공업으로 작성한 코드고, 하나의 예시다. 

    1. HighClass는 LowClass 인터페이스에 의존한다.
    2. LowClassV1은 IndexOutOfBoundsException / LowClassV2는 AcceptPendingException을 던진다.
    3. HighClass는 어떤 에러가 오는지를 확인해서 각각에 맞게 catch를 해야한다. 만약 그렇게 하지 않으면 런타임에 의도치 않은 에러가 발생할 수 있다

    HighClass는 LowClass 인터페이스에 의존하지만, 구현체의 버전이 바뀔 때 마다 HighClass도 그 변경점을 고려해서 코드가 바뀌어야 함을 의미한다. 이런 것들은 저수준 예외를 고수준 예외로 추상화 해주면서 해결할 수 있다. 조금 억지스러운 코드일 수 있지만, 아래처럼 AcceptPendingException이라는 예외를 잡아서 RuntimeException이라는 추상화 된 예외로 바꿔서 던지면 HighClass에서는 RuntimException만 잡아서 처리하면 된다.

    public class GoodExceptionTranslator {
    
        @RequiredArgsConstructor
        static class GoodHighClass {
            private final GoodLowClass lowClass;
            public void doSomething(){
                try {
                    lowClass.throwError();
                } catch (RuntimeException e) {
                    System.out.println();
                }
            }
        }
    
        static interface GoodLowClass {
            void throwError();
        }
    
        static class GoodLowClassV2 implements GoodLowClass {
            @Override
            public void throwError() {
                try {
                    throw new AcceptPendingException();
                } catch (AcceptPendingException e) {
                    throw new RuntimeException();
                }
                
            }
        }
    
        static class GoodLowClassV1 implements GoodLowClass {
            @Override
            public void throwError(){
                try {
                    throw new IndexOutOfBoundsException();
                } catch (AcceptPendingException e) {
                    throw new RuntimeException();
                }
            }
        }
    }

     


    디버깅에 도움된다면 예외 번역 시, 저수준 예외를 예외 연쇄하자.

    저수준 예외는 고수준 예외로 번역해서 던지는 것이 좋다고 했다. 그렇지만 만약 저수준 예외가 디버깅에 도움된다면, 고수준 예외로 번역해서 던질 때 저수준 예외를 포함시키는 것도 좋은 방법이 될 수 있다. 

    public void test() {
        try {
            System.out.println("true = " + true);
        } catch (IndexOutOfBoundsException cause) {
            cause.getCause(); // 필요하면 이걸로 확인.
            // cause.initCause(); // 필요하면 이걸로 cause 추가.
            throw new RuntimeException(cause);
        }
    }

    예외 연쇄는 위 코드처럼 사용할 수 있다. 

    • 고수준 예외 생성자에 저수준 예외를 넣어서 위로 전달할 수 있다. 이 예외를 받은 사람이 만약 저수준 예외를 확인하고 싶다면, getCause() 메서드를 호출하기만 하면 된다. 
    • 만약 처음부터 어떤 예외가 원인인지 정하고 싶다면 initCause() 메서드를 이용하면 된다. 

    댓글

    Designed by JB FACTORY