SuppressedException, Cause 그리고 예외 체이닝

박진형·2022년 5월 15일
0

억제된 예외


예외에서 사용할 수 있는 함수 중 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과 메시지로 예외를 확연하게 계층별로 구별할 수 있다.

0개의 댓글