예외를 만났을 때 취할 수 있는 태도는 복구와 회피, 전환으로 갈린다. 무엇을 고를지는 상황이 정한다.

예외 복구

예외가 나도 애플리케이션은 정상적으로 동작하게 만든다. 예외를 예측해서 다른 비즈니스 로직 흐름으로 유도하거나, 예외가 나지 않는 상태로 되돌리는 로직을 넣는다. try-catch-finally 블록으로 예외 상황을 파악하고 정상 상태로 돌려놓는 방식이다.

네트워크 환경이 좋지 않아 서버에 접속이 안 되는 상황이 대표적이다.

최대 5회까지 재시도
  성공하면 반환
  실패하면 로그 남기고, 원상 복구하고, 잠시 대기
  자원 정리
최대 횟수를 넘기면 예외를 던지거나 실패 페이지로 전환

재시도할 수 있는 성격의 문제일 때만 쓸 수 있다.

예외 회피

throws를 붙여 예외 처리를 직접 담당하지 않고 호출한 쪽으로 던진다.

정말 최선일 때만 써야 한다. 긴밀하게 역할을 나눈 관계가 아니라면 무책임한 행동이다. 자기가 처리할 수 있는데 떠넘기는 것이기 때문이다. 호출하는 프로그래머가 각자 프로그램에 맞게 처리하도록 유도하는 경우라면 괜찮다.

예외 전환

의미가 분명한 새 예외로 바꿔서 던진다. 호출하는 쪽이 어떤 예외인지 알아보기 쉬워져 처리가 수월해진다.

복구 불가능한 Checked Exception을 잡아 Unchecked로 바꾸면 다른 계층들이 일일이 throws를 선언하지 않아도 된다. 클라이언트 단은 SQLException을 받아봐야 어떻게 처리해야 할지 모른다. 서버가 의미 있는 메시지로 바꿔 정확하게 알려주고 싶을 때 쓰면 유용하다. 스프링에서 로그인 5회 이상 실패나 잘못된 이미지 파일 확장자 검사를 Unchecked로 처리하는 것이 일반적인 이유도 같다.

catch 블록의 순서

catch 블록이 여러 개여도 실행되는 것은 하나뿐이다. try 안에서 예외가 동시다발적으로 나지 않고, 하나가 나면 즉시 실행을 멈추고 해당 블록으로 가기 때문이다. 그래서 상위 예외 클래스가 더 상세한 하위 예외보다 아래에 와야 한다.

catch (ArrayIndexOutOfBoundsException e) { }   // 구체적인 것 먼저
catch (Exception e) { }                        // 넓은 것 나중

catch는 위에서부터 차례대로 검색되는데, 하위 예외는 상위 예외를 상속했으므로 상위 타입이기도 하다. Exception을 위에 두면 아래 블록은 영영 실행되지 않고 컴파일 오류가 난다.

멀티 catch의 제약

|로 여러 예외를 한 블록에서 처리하는 멀티 catch에도 제약이 있다. 부모와 자식 관계인 예외를 함께 쓸 수 없다. 다형성 때문에 RuntimeException 하나로 ArithmeticException까지 이미 처리되기 때문이다. 또 참조 변수 하나로 여러 예외를 받으므로 공통 조상의 멤버만 부를 수 있다. 특정 예외의 메서드를 쓰려면 instanceof로 판단해 캐스팅해야 하는데, 그럴 바에는 catch 블록을 나눠 쓰는 편이 낫다.

catch (IOException | SQLException e) { }

printStackTrace 대신 로깅

printStackTrace()는 익숙하지만 실무에서는 피한다. System.err로 쓰여서 제어하기가 힘들고 출력이 어디로 가는지 파악하기 어렵다. 톰캣이라면 catalina.out에 남는다. 자바 리플렉션으로 추적하는 것이라 오버헤드가 크고, 서버에서 스택 정보를 취합하므로 서버에 부하가 생기기도 한다. 스프링 서버 콘솔에 찍히는 많은 내용 사이에서 그 출력을 찾는 것도 일이다. log4j, logback, slf4j 같은 로깅 라이브러리로 남기면 로그 패턴과 메시지를 지정해 콘솔 로그나 파일 로그로 관리할 수 있다.

스택 트레이스 노출

보안 쪽으로도 걸린다. printStackTrace()는 CWE-497(Exposure of Sensitive System Information)에 등록된 취약점이다. 예외가 나면 발생지 근원부터 파일 경로까지 콘솔에 찍히고 자바 레벨의 순수 메서드까지 호출 경로가 드러나는데, 그 내용이 외부인에게 제공될 수 있다. 디버깅하면서 넣은 출력 코드는 반드시 모두 지워야 한다.

관련

출처