BackEnd/Java

[Java] 예외 계층과 실무에서의 예외 처리 방법

kangminhyuk1111 2024. 7. 17. 14:40

언체크 예외는 체크 예외와 기본적으로 동일하다. 차이가 있다면 예외를 던지는 throws를 선언하지 않고 생략 할 수 있다. 생략한 경우 자동으로 예외를 던진다.

RuntimeException을 상속받은 클래스는 언체크 예외가 된다.

언체크 예외는 컴파일러가 체크 안하기 때문에 throws로 나열하지 않아도 됨.

예외도 객체다. 필요한 필드와 메서드를 가질 수 있다.

try catch finally

try -> 정상적인 흐름
catch -> 예외 흐름
finally -> 마무리 흐름

finally 선언시 try catch블록의 흐름에 상관없이 finally 안의 로직은 반드시 실행하게 된다.

try {  
    client.connect();  
    client.send(data);  
} catch (NetworkClientExceptionV2 e) {  
    System.out.println("[오류] 코드: " + e.getErrorCode() + ", 메세지: " + e.getMessage());  
} finally {  
    client.disconnect();  
}

예외 계층

예외를 단순히 오류 코드로 분류하는 것이 아니라, 예외를 계층화 해서 다양하게 만들면 더 세밀하게 예외에 대한 처리를 할 수 있다.

자바에서는 예외 조차도 객체이다. 부모예외를 잡으면 자식 예외도 함께 잡을 수 있다.

특정 예외를 잡고 싶으면 하위 예외, 즉 자식 예외를 잡아서 처리할 수 있다.

catch문을 다중으로 사용할 때, 부모 예외 객체가 exception이고 자식 예외 객체가 custom exception이라면, catch문에는 custom exception이 먼저 와야한다. 먼저 처리하지 않을 시 컴파일 에러가 생긴다.

이유는 부모 객체에서 에러 처리를 먼저하면 자식 객체도 포함되기 때문에 존재 할 필요가 없는 catch문 이라고 인식하게 되어버리기 때문에 에러를 발생 시킨다.

예외를 계층화 하고 다양하게 만들면 더 세밀한 동작들을 깔끔하게 처리할 수 있다. 그리고 특정 분류의 공통 예외들도 한번에 catch로 잡아서 처리할 수 있다.

실무 예외 처리 방안 1

처리 할 수 없는 예외

네트워크 서버에 문제가 발생해서 통신이 불가능 하거나, db 서버에 문제가 발생해서 접속이 안되면, 애플리케이션에서 연결 오류, 데이터 베이스 접속 실패와 같은 예외가 발생한다.

이러한 경우 상대 측의 에러이기 때문에 애플리케이션 단에서 할 수 있는 예외처리가 없다.
아무리 다시 시작해도 같은 오류를 반복 할 뿐이다.

이런 경우에는 시스템에 문제가 있다. 라는 오류 메세지를 보여주고, 만약 웹이라면 오류 페이지를 보여주게 한다. -> 애플리케이션에서 해결할 수 없기 때문에 다른 방법으로 오류를 해결한 경우

결론적으로 체크 예외에 대한 부담이 생기게 된다.
컴파일러가 체크 해주는 체크 예외들은 많이 사용 되었지만 결론적으로 실무에서는, 처리할 수 없는 예외가 많아지고, 프로그램이 복잡해지면서 체크 예외를 사용하는 것이 부담이 되었다.

Service 객체가 모두 체크 예외를 받아서 처리하면 catch문이 중첩으로 엄청나게 많이 생기게 될 것이다.

ex) DbException, NetworkException, XxxException 등등 문제가 생긴 경우.. 모두 체크 예외로 하면 catch문이 3개 혹은 다른 exception이 추가 될 때 마다 더 많은 예외처리가 필요하다.

결론적으로, Service에서 처리할 수 없는 모든 exception들은 그냥 "알 수 없는 오류 입니다." 이거 하나로 처리해도 상관 없을 것이다.

애플리케이션에서 처리 가능한 에러는 catch를 사용하여 처리하고, 만약 처리할 수 없는 외부에 의존하는 exception은 하나의 에러로 예측하고 밖으로 던지는게 효율적이다.

그렇다고 해서 모든 exception의 부모인 Exception 객체를 위로 한번에 던져버리는 것은 매우 바람직하지 못한 코드이다.

왜냐하면, 모든 exception을 최상위 부모로 처리 했을때, 실제로 우리가 애플리케이션에서 처리해야될 exception 조차, unchecked exception으로 처리 되어버리기 때문에 매우 안좋은 코드가 되어버린다. 에러가 왜 생긴지 조차 디버깅하기 어려워지는 상황이 되어버린다.

그렇다면 일일이 명시하는 것과, 하나로 통일 시키는 것의 중간점을 찾아서 해결하는 것이 중요 할 것이라고 생각이 든다.

예외 공통 처리

애플리케이션에서 어떠한 행동을 해도 해결할 수 없는 예외는 공통적으로 처리해준다.

// 공통 예외 처리  
private static void exceptionHandler(final Exception e) {  
    System.out.println("사용자 메시지: 죄송합니다. 알 수 없는 문제가 발생했습니다.");  
    System.out.println("==개발자용 디버깅 메시지==");  
    e.printStackTrace(System.out);  

    if(e instanceof SendExceptionV4 sendEx) {  
        System.out.println("[전송 오류] 전송 데이터: " + sendEx.getSendData());  
    }  
}

사용자가 디테일한 오류 코드나 오류 상황을 모두 이해할 필요가 없기 때문에 Exception을 받아서 사용자에게는 그냥 문제가 있습니다. 이정도만 설명을 하고 개발자는 빠른 대응을 위해 오류 메세지를 남긴다.

다시 말하지만 예외도 객체이기 때문에, instanceof와 같이 객체의 타입을 확인 할 수 있다.

e.printStackTrace()

  • 예외 메세지와 스택 트레이스를 출력 할 수 있다.
  • 이 기능을 사용하면 예외가 발생한 지점을 역으로 추적할 수 있다.

최근 많은 라이브러리들은 unchecked exception 처리를 선호한다.

try-with-resources

애플리케이션에서 외부 자원을 사용하는 경우 반드시 외부 자원을 해제해야 한다.

finally 없이 자동으로 리소스를 반환한다.

  • 리소스 누수 방지 : 모든 리소스가 제대로 닫히도록 보장한다.
  • 명시적인 close 호출이 필요없어 코드가 간결하다.
  • try가 끝나고 catch이전에 자원을 반납하기 때문에 finally 보다 조금 더 빠르다.
반응형