억제된 예외

예외에서 사용할 수 있는 함수 중 addSuppressed()라는 함수가 있다.
이것을 어디서 어떻게 활용하는지 궁금했는데 다음과 같다.
public static void main(String[] args) { try { throw new IllegalAccessException("IllegalArgumentException"); } catch (IllegalAccessException e){ throw new NullPointerException("NullPointerException"); }finally { throw new ArithmeticException("ArithmenticException"); } }
간단한 예제로 이 코드를 실행해 보았을 때, 3개의 예외가 발생하지만 최종적으로 다음과 같이 ArithmeticException만 출력된다.

예외를 던질 때는 최종적으로 하나의 예외만 던질 수 밖에 없는데, addSuppressed()라는 최종적으로 던질 예외를 제외한 예외들을 억제된 예외로 추가할 수 있다.
- 억제된 예외 사용
public static void main(String[] args) throws Exception { BufferedReader reader = null; Exception optionalException = null; try { reader = new BufferedReader(new FileReader("no file")); } catch (IOException ioException) { optionalException = ioException; } finally { try{ reader.close(); // NPE }catch (NullPointerException npe) { optionalException.addSuppressed(npe); throw optionalException; } } }
실행 해보면 다음과 같이 NullPointerException이 억제된 예외로 등록되고 최종적으로 FileNotFountException이 throw된다.

그리고 이 억제된 예외는 try-with-resource에서도 사용되는데 다음과 같다.
- try-with-resource에서의 억제된 예외
class DirtyResource implements AutoCloseable { public void accessResource() { throw new RuntimeException("It's a dirty Resource!!!"); } @Override public void close() throws Exception { throw new NullPointerException("null pointer exception!!"); } } public class Exception3 { public static void main(String[] args) throws Exception { try (DirtyResource resource= new DirtyResource()) { resource.accessResource(); } } }
DirtyResource는 AutoCloseable을 implements받아 try-with-resource구문에서 사용되면 자동으로 close함수가 실행된다. 이 때, NullPointerException이 발생한다고 가정을한다.그리고 accessResource() 함수를 실행하면 예외를 발생하도록 했을 때 다음과 같이 결과값이 나온다.

accessResource() 함수가 실행되며 RuntimeException이 발생되고 그 다음 close함수가 실행되는데, 이 때 close함수에서도 예외가 발생하면 accessResource에서 발생된 RuntimeException에 억제된 예외로 등록되는 것이다.
Cause
cause는 예외의 원인이 되는 상세 예외를 뜻한다.Enum의 valueOf() 함수를 사용했을 때 커스텀 예외를 던지고자 했을 때 이 커스텀 예외에 원인이 되는 예외를 등록해 상세한 예외를 명세할 수 있다.
public static VoucherType fromString(String name) { try { return VoucherType.valueOf(name); } catch(IllegalArgumentException e) { throw new NotValidEnumTypeException("바우처 타입을 확인하세요 (Fixed, Percent)", e); } }
- Caused by로 원인 예외가 등록된 것을 볼 수 있다.

예외 체이닝
상위 계층으로 예외가 전달될 때 예외를 새로운 예외에 포함시켜 전달하는 과정을 예외 체이닝, 래핑이라고 한다.
예외 체이닝이 필요한 이유?
예외 체이닝의 주요 목적은 본래의 예외를 여러 계층에 걸쳐 전달될 때 보존해주기 위해서다.예외를 보존함으로써 예외 스택을 추적하기 쉬워지면서 디버깅할 때 유용하다.다음은 예외 체이닝을 제대로 사용하지 않았을 경우다.
예외 체이닝이 부적절한 경우
아래와 같은 계층 구조를 가졌을때를 예로 들어본다.

public class ExceptionChaining { public static void main(String[] args) { try { new SomeService().findSomeData(); } catch (SQLException e) { e.printStackTrace(); } } }
public class SomeService { public void someBusiness() throws SQLException { try { new SomePersistence().get(); } catch (SQLException e) { throw e; } } }
public class SomePersistence { public SomeData get() throws SQLException { try { SomeData someData = new SomeData(); DataBaseUtils.execute("bad Sql"); return someData; } catch (SQLException e) { throw e; } } }

예제와 같은 경우에는 3가지의 메소드 밖에 실행하지 않았기 때문에 계층 구조를 파악하는게 어렵지 않은데? 라고 생각할 수 있겠지만 많은 메소드를 거치는 경우 계층을 구별할 수 없어 디버깅할 때 어려움을 겪을 수도 있다.
이러한 방식보다 조금 더 개선된 방식을 제안한다.
public class ExceptionChaining { public static void main(String[] args) { try { new SomeService().findSomeData(); } catch (SomeServiceException e) { e.printStackTrace(); } } }
public class SomeService { public void findSomeData() { try { new SomePersistence().get(); } catch (PersistenceException e) { throw new SomeServiceException("There is no some data", e); } } }
public class SomePersistence { public SomeData get() { try { SomeData someData =new SomeData(); DataBaseUtils.execute("bad SQL"); return someData; } catch (SQLException e) { throw new PersistenceException("query some Data from DB Error", e); } } }
public class DataBaseUtils { public static void execute(String sql) throws SQLException { throw new SQLException("Syntax Error"); } }

이제 Custom Exception과 메시지로 예외를 확연하게 계층별로 구별할 수 있다.