예외에서 사용할 수 있는 함수 중 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에서도 사용되는데 다음과 같다.
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는 예외의 원인이 되는 상세 예외를 뜻한다.
Enum의 valueOf() 함수를 사용했을 때 커스텀 예외를 던지고자 했을 때 이 커스텀 예외에 원인이 되는 예외를 등록해 상세한 예외를 명세할 수 있다.
public static VoucherType fromString(String name) {
try {
return VoucherType.valueOf(name);
} catch(IllegalArgumentException e) {
throw new NotValidEnumTypeException("바우처 타입을 확인하세요 (Fixed, Percent)", e);
}
}
상위 계층으로 예외가 전달될 때 예외를 새로운 예외에 포함시켜 전달하는 과정을 예외 체이닝, 래핑이라고 한다.
예외 체이닝의 주요 목적은 본래의 예외를 여러 계층에 걸쳐 전달될 때 보존해주기 위해서다.
예외를 보존함으로써 예외 스택을 추적하기 쉬워지면서 디버깅할 때 유용하다.
다음은 예외 체이닝을 제대로 사용하지 않았을 경우다.
아래와 같은 계층 구조를 가졌을때를 예로 들어본다.
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과 메시지로 예외를 확연하게 계층별로 구별할 수 있다.