10. 예외 처리 - 실습
예제 샘플

아래 샘플 프로그램은 반환 값을 사용해서 예외를 처리했다. 이런 경우 다음과 같은 문제가 있었다.
정상 흐름과 예외 흐름이 섞여 있기 때문에, 코드를 한눈에 이해하기 어렵다. 쉽게 이야기해서 가장 중요한 정상 흐름이 한눈에 들어오지 않는다.
심지어 예외 흐름이 더 많은 코드 분량을 차지한다. 실무에서는 예외 처리가 훨씬 더 복잡하다.
예외 처리 도입5 - finally
자바 예외처리를 통해서 변경된 내용은 다음과 같다.
정상 로직과 오류 로직이 명확하게 구분되었다.
공통적으로 처리해야하는 로직을
finally블록 안에서 처리가 가능하다.
예외 계층1 - 시작

예외를 단순히 오류 코드로 분류하는 것이 아니라, 예외를 계층화해서 다양하게 만들면 더 세밀하게 예외를 처리할 수 있다.
NetworkClientExceptionV3:NetworkClient에서 발생하는 모든 예외는 이 예외의 자식이다.ConnectExceptionV3: 연결 실패시 발생하는 예외이다. 내부에 연결을 시도한address를 보관한다.SendExceptionV3: 전송 실패시 발생하는 예외이다. 내부에 전송을 시도한 데이터인sendData를 보관한다.
이렇게 예외를 계층화하면 다음과 같은 장점이 있다.
자바에서 예외는 계층이다. 따라서 부모 예외를 잡거나 던지면, 자식 예외도 함께 잡거나 던질 수 있다.
예를 들어,
NetworkClientExceptionV3예외를 잡으면 그 하위인ConnectExceptionV3,SendExceptionV3예외도 함께 잡을 수 있다. (다형성 활용)
특정 예외를 잡아서 처리하고 싶으면
ConnectExceptionV3,SendExceptionV3와 같은 하위 예외를 잡아서 처리하면 된다.
이제 예외 계층을 만들어보자
예외 계층2 - 활용
아래 NetworkServiceV3_2 코드를 살펴보면 예외 계층을 활용하여 필요에 따라 유연하게 예외를 처리하는 것을 확인할 수 있다.
정리
예외를 계층화하고 다양하게 만들면 더 세밀한 동작들을 깔끔하게 처리할 수 있다. 그리고 특정 분류의 공통 예외들도 한번에 catch 로 잡아서 처리할 수 있다.
실무 예외 처리 방안1 - 설명
처리할 수 없는 예외
예를 들어, 상대 네트워크 서버에 문제가 발생해 통신이 불가하거나, 데이터베이스 서버에 문제가 발생해서 접속이 안되면, 애플리케이션에서 연결 오류, 데이터베이스 접속 실패와 같은 예외가 발생한다.
이렇게 시스템 오류 때문에, 발생한 예외들은 대부분 예외를 잡아도 어플리케이션 단에서 처리가 불가능하다.. 예외를 잡아서 다시 호출을 시도해도 같은 오류가 반복될 뿐이다..
이런 경우 고객에게는 "현재 시스템에 문제가 있습니다" 라는 오류 메시지를 보여주고, 만약 웹이라면 오류 페이지를 보여주면 된다. 그리고 내부 개발자가 문제 상황을 빠르게 인지할 수 있도록, 오류에 대한 로그를 남겨두어야 한다.
체크 예외의 부담
체크 예외는 개발자가 실수고 놓칠 수 있는 예외들을 컴파일러가 체크해주기 때문에 오래전부터 많이 사용되었다.
그런데 앞서 설명한 것처럼 처리할 수 없는 예외가 많아지고, 또 프로그램이 점점 복잡해지면서 체크 예외를 사용하는 것이 점점 더 부담스러워졌다.
체크 예외 사용 시나리오
체크 예외를 사용하게 되면 어떤 문제가 발생하는지 가상의 시나리오로 이야기하겠다.

실무에서는 수 많은 라이브러리를 사용하고, 또 다양한 외부 시스템과 연동한다.
사용하는 각각의 클래스들이 자신만의 예외를 모두 체크 예외로 만들어서 전달한다고 가정하자.
이 경우
Service는 호출하는 곳에서 던지는 체크 예외들을 처리해야 한다. 만약 처리할 수 없다면 밖으로 던져야 한다.
모든 체크 예외를 잡아서 처리하는 예시
그런데 앞서 설명했듯이 상대 네트워크 서버가 내려갔거나, 데이터베이스 서버에 문제가 발생한 경우 Service 에서 예외를 잡아도 복구할 수 없다..
Service에서는 어차피 본인이 처리할 수 없는 예외들이기 때문에, 던지는 것이 나은 결정이다.
모든 체크 예외를 던지는 예시
이렇게 모든 체크예외를 하나씩 다 밖으로 던져야한다.
라이브러리가 늘어날수록 다루어야 하는 예외도 더 많아진다. 개발자 입장에서는 이것은 상당히 번거로운 일이 된다.
문제는 여기서 끝이 아니다. 만약 중간 지점에 Facade 라는 클래스가 있다고 가정해보자.

이 경우,
Facade클래스에서도 이런 예외들을 복구할 수 없다.Facade클래스도 예외를 밖으로 던져야 한다.결국 중간에 모든 클래스에서 예외를 계속 밖으로 던지는 지저분한 코드가 만들어진다..
throws Exception
개발자는 본인이 다룰 수 없는 많은 체크 예외 지옥에 빠지게 된다. 결국 다음과 같이 최악의 수를 두게 된다.
이렇게 하면 Excpetion 은 물론이고 그 하위 타입인 NetworkException, DatabaseException 도 함께 던지게 된다. 그리고 이후에 예외가 추가되더라도 throws Exception 은 변경하지 않고 그래도 유지할 수 있다. 코드가 깔끔해지는 것 같지만 치명적인 문제가 있다..
throws Exception 의 문제
Exception 은 최상위 타입이므로 모든 체크 예외를 다 밖으로 던지는 문제가 발생한다.
결과적으로, 체크 예외의 최상위 타입인 Exception 을 던지게 되면 다른 체크 예외를 체크할 수 있는 기능이 무효화 되고, 중요한 체크 예외를 다 놓치게 된다. 중간에 중요한 체크 예외가 발생해도 컴파일러는 Exception 을 던지기 때문에, 문법에 맞다고 판단해서 컴파일 오류가 발생하지 않는다.
쉽게 이야기해서 어떤 오류가 발생했는지, 확인할 수가 없다..
문제 정리
지금까지 알아본 체크 예외를 사용할 때 발생하는 문제들은 다음과 같다.
처리할 수 없는 예외
체크 예외의 부담
사실 Service 를 개발하는 개발자 입장에서 수 많은 라이브러리에서 쏟아지는 예외를 다 다루고 싶지는 않을 것이다.
특히 본인이 해결할 수도 없는 모든 예외를 다 다루고 싶지는 않을 것이다.
본인이 해결할 수 있는 예외만 잡아서 처리하고, 본인이 해결할 수 없는 예외는 신경쓰지 않는 것이 더 나은 선택일 수 있다.
언체크(런타임) 예외 사용 시나리오

이번에는
Service에서 호출하는 클래스들이 언체크(런타임) 예외를 전달한다고 가정해보자.NetworkException,DatabaseExceptio은 잡아도 복구할 수 없다. 언체크 예외이므로 무시하면 된다. (언체크 예외이므로throws를 선언하지 않아도 된다)
예외 공통 처리
이렇게 처리할 수 없는 예외들은 중간에 여러곳에서 나누어 처리하기 보다는 예외를 공통적으로 처리할 수 있는 공간을 만들어서 한 곳에서 처리하면 된다.
어차피 해결할 수 없는 예외들이기 때문에, 이런 경우 고객에게는 현재 시스템에서 문제가 있습니다. 라고 오류 메시지를 보여주고, 만약 웹이라면 오류 페이지를 보여주면 된다.
그리고 개발자가 상황을 빠르게 인지할 수 있도록, 오류에 대한 로그를 남겨두면 된다.
실무 예외 처리 방안2 - 구현

NetworkClientExceptionV4는 언체크 예외인RuntimeException을 상속 받는다.이제
NetworkClientExceptionV4와 자식은 모두 언체크 예외가 된다.
구현 코드
이런 방식 덕분에 NetworkServiceV4 는 해결할 수 없는 예외 보다는 본인 스스로의 코드에 더 집중할 수 있다.
따라서 코드가 깔끔해진다.
try-with-resources
애플리케이션에서 외부 자원을 사용하는 경우 반드시 외부 자원을 해제해야 한다.
따라서 finally 구문을 반드시 사용해야 한다.
try 에서 외부 자원을 사용하고, try 가 끝나면 외부 자원을 반납하는 패턴이 반복되면서 자바에서는 Try with resources라는 편의 기능을 자바 7에서 도입했다. 이름 그대로 try 에서 자원을 함께 사용한다는 뜻이다. 여기서 자원은 try 가 끝나면 반드시 종료해서 반납해야 하는 외부 자원을 뜻한다.
이 기능을 사용하려면 먼저 AutoCloseable 인터페이스를 구현해야 한다.
이 인터페이스를 구현하면 Try with resources를 사용할 때
try가 끝나는 시점에close()가 자동으로 호출된다.
그리고 다음과 같이 Try with resources 구문을 사용하면 된다.
Try with resources 장점
리소스 누수 방지 : 모든 리소스가 닫히도록 보장한다. 실수로
finally블럭을 적지 않거나,finally블럭 안에서 자원 해제 코드를 누락하는 문제들을 방지할 수 있다.코드 간결성 및 가독성 향상 : 명시적인
close()호출이 필요 없어 코드가 더 간결하고 읽기 쉬워진다.스코프 범위 한정 : 범위가 직관적으로 좁하지기 때문에, 유지보수가 용이해진다.
조금 더 빠른 자원 해제 : 기존에는
try->catch->finally로catch이후에 자원을 반납했다. Try with resources 구분은try블럭이 끝나면 즉시close()를 호출한다.
정리
처음 자바를 설계할 당시에는 체크 예외가 더 나은 선택이라 생각했다. 그래서 자바가 기본으로 제공하는 기능들에는 체크
예외가 많다. 그런데 시간이 흐르면서 복구 할 수 없는 예외가 너무 많아졌다. 특히 라이브러리를 점점 더 많이 사용 하면서 처리해야 하는 예외도 더 늘어났다. 라이브러리들이 제공하는 체크 예외를 처리할 수 없을 때마다 throws 에 예외를 덕지덕지 붙어야 했다.
그래서 개발자들은 throws Exception 이라는 극단적(?)인 방법도 자주 사용하게 되었 다. 물론 이 방법은 사용하면
안된다. 모든 예외를 던진다고 선언하는 것인데, 결과적으로 어떤 예외를 잡고 어떤 예외를 던지는지 알 수 없기 때문이다.
체크 예외를 사용한다면 잡을 건 잡고 던질 예외는 명확하게 던지도록 선언해야 한다.
체크 예외의 이런 문제점 때문에 최근 라이브러리들은 대부분 런타임 예외를 기본으로 제공한다. 가장 유명한 스프링이나 JPA 같은 기술들도 대부분 언체크(런타임) 예외를 사용한다.
런타임 예외도 필요하면 잡을 수 있기 때문에 필요한 경우에는 잡아서 처리하고, 그렇지 않으면 자연스럽게 던지도록 둔다. 그리고 처리할 수 없는 예외는 예외를 공통으로 처리하는 부분을 만들어서 해결하면 된다.
Last updated