본문 바로가기
Programming Language 이해하기/Java 이해하기

예외를 처리하는 Best Practice는 무엇일까?(with. 토비 스프링, 이펙티브 자바)

by simplify-len 2021. 1. 4.

들어가기

 예외를 어떻게 하면 우아하게 처리할 수 있을까?

위 질문의 답을 찾기 위해- 이제부터 토비 스프링의 책 내용과 이펙티브 자바의 내용을 소개하고자 한다.

예외 블랙홀

<문제>

그림1 - 토비의 스프링 출처

예외를 잡고 아무 행위도 하지 않는다. 이것은 굉장히 위험한 행위이다.

그림2 - 토비의 스프링 출처

이런 코드도 마찬가지로 위험한 코드이다

그렇다면? 어떻게 해야될까?

예외를 처리할 때는 모든 예외는 적절하게 복구되든지 아니면 작업을 중단시키고 운영자 또는 개발자에게 분명하게 통보돼야 한다.

또한 Throws 는 무의하고 무책임하다. 바로 아래와 같은 코드를 말한다.

그림3 - 토비의 스프링 출처

위 두 가지 나쁜 습관은 어떤 경우에도 용납하지 않아야 한다.


Item77. 예외를 무시하지 말라. - 이펙티브 자바

왜 우리는 예외를 명시하는 이유가 무엇인가?

API 설계자에게 메서드 선언에 예외를 명시하는 이유는 그 메서드를 사용할 때 적절한 조치를 취해달라고 말하는 것이다. 그러므로 우리는 예외를 무시하면 안된다.

// catch 블록을 비워두면 예외가 무시된다. 아주 의심스러운 코드다.

try { ... } catch (SomeException e){ }

예외는 문제 상황에 잘 대처하기 위해 존재하는데 catch 블록을 비워두면 예외가 존재할 이유가 없어진다.
당연히 무시해야 할 때도 있다. FileInputStream 을 닫을 때, 파일의 상태를 변경하지 않았으니 복구할 것이 없으며, 필요한 정보는 이미 다 읽었다는 뜻이니 남은 작업을 중단할 이유가 없다. 그러므로, 예외를 무시하기로 했다면 catch 블록 안에 그렇게 결정한 이유를 주석으로 남기고 예외 변수의 이름도 ignored로 바꿔놓도록 하자.

try{
...
} catch (TimeoutException | ExecutionException ignored){
// 기본값을 사용한다(색상 수를 최소화하면 좋지만, 필수는 아니다.) 
}

이 내용은 검사와 비검사 예외 모두에 똑같이 적용된다. 예측할 수 있는 예외 상황이든 프로그래밍 오류든, 빈 catch 블록으로 못 본 척 지나치면 그 프로그램은 오류를 내재한 채 동작하게 된다.
그런 작은 코드들이 쌓이다보면, 아무 상관없는 코드 때문에 죽을 수도 있다.
예외를 적절히 처리하면 오류를 완전히 피할 수도 있다. 무시하지 않고 바깥으로 전파되게만 놔둬도 최소한 디버깅 정보를 남긴 채 프로그램이 신속히 중된되게는 할 수 있다.

 

그렇다면 예외의 종류와 특징은 무엇일까?

  • Error

    Java VM 위에서 발생하는 에러로서, 예를 들어 OutOfMemoryError 또는 ThreadDeath 같은 에러를 말한다. 이같은 에러는 catch로 잡을 수 없을 뿐만 아니라, 아무런 대응 방법이 없다.

Exception과 체크 예외

java.lang.Exception 클래스와 그 서브클래스로 정의되는 예외들은 애플리케이션 코드의 작업중 발생한다.

Exception 클래스는 다시 체크 예외와 언체크 예외로 구분

그림1 - 토비의 스프링 출처

  • RuntimeException과 언체크/런타임 예외

    • Java.lang.RuntimeExcpetion 클래스를 상속한 예외들은 명시적인 예외 처리를 강제하지 않기 때문에 UnChecked 예외라 불린다.

    • 런타임 예외는 주로 프로그램의 오류가 있을 때 발생하도록 의도된 것

    • 피할 수 있지만 개발자가 부주의해서 발생할 수 있는 경우에 발생하도록 만든 것이 런타임 예외

    • 예상하지 못했던 예외상황에서 발생하는 게 아니기 때문에 굳이 catch나 throws를 사용하지 않아도 되로록 만든 것

그럼 예외를 처리는 방법으로는?

예외 상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것

예외처리 회피

: 예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져버리는 것

  • throws 문으로 선언해서 예외가 발생하면 알아서 던져지게 하거나 catch문으로 일단 에외를 잡은 후에 로그를 남기고 다시 예외를 던지는(rethrow)것

  • 예외처리를 회피는 자칫하면 성의없는 throws SQLException과 같이 구체적인 예외를 던지도록 선언하기가 귀찮아 throws Exception을 사용할 가능성이 높다.

  • 그러므로 예외를 회피하는 것은 예외를 복구하는 것처럼 의도를 분명하게 하자.

    1. 예외 전환 - 예외 회피와 비슷하게 예외를 복구해서 정상적인 상태로는 만들 수 없기 때문에 예외를 메소드 밖으로 던지는 것이다. 하지만 예외 회피와 달리, 발생한 예외를 그대로 넘기는 게 아니라 적절한 예외로 전환해서 던진다 주로 2가지 목적이 존재한다.

      1. 내부에서 발생한 예외를 그대로 던지는 것이 그 예외상황에 대한 적절한 의미를 부여해주지 못하는 경우. 의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위해서, API가 발생하는 기술적인 로우레벨을 상황에 적합한 의미를 가진 예외로 변경하는 것. 예를 들어 특정 기술의 정보를 해석하는 코드를 비지니스 로직을 담은 서비스 계층에 두는 건 매우 어색(SQLException -> DuplicateUserIdException)

      2. 예외를 처리하기 쉽고 단순하게 만들기 위해 포장(warp)하는 것. 중첩 예외를 이용해 새로운 예외를 만들어 원인이 되는 예외를 내부에 담아서 던지는 방식 - 주로 예외처리를 강제하는 체크 예외를 언체크 예외인 런타인 예외로 바꾸는 경우에 사용

예외를 처리하는 전략으로 무엇이 있는가?

런타임 예외의 보편화

  • 일반적으로 체크 예외가 일반적인 예외를 다루고, 언체크 예외는 시스템 장애나 프로그램상의 오류로 사용

  • 체크예외는 catch, throws선언을 강제하는 데 이는 문제가 될 수 있다.

  • 그러므로, 항상 복구할 수 있는 예외가 아니라면 일단 Unchecked 예외로 만드는 경향이 있다. 대게는 복구 불가능한 상황이고 보나마나 RuntimeException 등으로 포장해서 던져야 할 테니 아예 API 차원에서 런타임 예외를 던지도록 만드는 것이다.

애플리케이션 예외

      • 런타임 예외 중심의 전략은 굳이 이름을 붙이자면 낙관적인 예외처리 기법이라고 할 수 있다. 일단 복구할 수 있는 예외는 없다고 가정하고 예외가 생겨도 어차피 런타임 예외이므로 시스템 레벨에서 알아서 처리해줄 것. 꼭 필요한 경우는 런타임 예외라도 잡아서 복구하거나 대응할 수 있다는 문제 될 것이 없다는 낙관적인 태도를 기반으로

      • 반면에 시스템 또는 외부의 예외상황이 원인이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 발생시키고, 반드시 catch해서 무엇인가 조치를 취하도록 요구하는 예외도 있다. 이런 예외들은 일반적으로 애플리케이션 예외

 

> 이펙티브 자바 3판에서도 예외에 대한 내용이 있다.

https://github.com/LenKIM/everyone-is-effective-java-study/wiki/ch10-%EC%98%88%EC%99%B8

 

LenKIM/everyone-is-effective-java-study

다우기술 연구소 스터디 저장소. Contribute to LenKIM/everyone-is-effective-java-study development by creating an account on GitHub.

github.com

 

댓글