Home Item71 (필요 없는 검사 예외 사용은 피하라)
Post
Cancel

Item71 (필요 없는 검사 예외 사용은 피하라)

필요 없는 검사 예외 사용은 피하라

이전 아이템인 아이템 70에서도 살짝 언급한 부분이다.

물론 검사 예외를 잘 활용하면 발생한 예외를 프로그래머가 처리하여 안전성을 높이고, 프로그램의 질을 높일 수 있다.

하지만 검사 예외를 과하게 사용하면 사용하기 불편한 API가 된다.

검사 예외를 사용하게 되면, catch 블록을 두어 그 예외를 붙잡아 처리하거나 더 바깥으로 던져 문제를 전파해야 한다. 바깥으로 전파하는 과정에서 던져지는 예외에 의존하게 되는 사태까지 벌어질 수 있다.

또한 두 경우 모두 API 사용자에게 부담을 준다. 더해 검사 예외를 던지는 메서드는 스트림 안에서 직접 사용할 수 없어 Java 8 이후부터는 그 부담이 더욱 커졌다.

  • API를 제대로 사용해도 발생할 수 있는 예외
  • 프로그래머가 의미있는 조치를 취할 수 있는 경우

위 두가지 경우라면 검사 예외를 사용해도 좋을 것이다. 하지만 그렇지 않다면 비검사 예외를 사용하는 것이 좋다.

1
2
3
} catch (TheCheckedException e) {
	throw new AssertionError();
}
1
2
3
4
} catch (TheCheckedException e) {
	e.printStackTrace();
	System.exit(1);
}
  • 이 두 가지 검사 예외를 처리하는 방법이 과연 맞는 방법일까?
  • 두 처리 방법 모두 당장 발생한 검사 예외를 잡는 역할만 수행할 뿐 근본적인 조치를 취하지 못했다.
  • 검사 예외는 프로그래머가 그 예외를 잘 다룰 수 있는 상황일 때 비로소 사용할 수 있다고 생각한다.
    • 더 나은 방법이 없다면 비검사 예외를 선택하면 된다.

단 하나의 검사 예외만 던질 때 부담은 더 커진다.

검사 예외는 프로그래머에게 그 예외를 처리해야 하는 부담감을 지운다.

이러한 부담은 메서드가 단 하나의 검사 예외만 던질 때 더 커진다.

이미 다른 검사 예외도 던지는 상황에서 또 다른 검사 예외를 추가하는 경우를 가정한다면, 기껏해야 catch문 하나 추가하는 정도밖에 되지 않는다.

하지만 검사 예외가 단 하나 뿐이라면, 그 예외 하나 때문에 API 사용자는 try 블록을 추가해야 하고 스트림에서 직접 사용하지 못한다.

따라서 이러한 경우 다른 방법을 생각해야 한다.

단 하나의 검사 예외만 던지는 경우의 대처법

단 하나의 검사 예외만 던지는 것은 부담이 크다는 것을 알았다.

이 상황을 회피하려면, 검사 예외를 던지지 않는 방법밖에 없다.

검사 예외를 던지지 않는 방법 2가지가 있다.

적절한 결과 타입을 담은 옵셔널을 반환한다.

  • 검사 예외를 밖으로 던지는 대신 단순하게 빈 옵셔널을 반환한다.
    • 이 방식의 단점은 예외가 발생한 이유를 알려주는 부가 정보를 담을 수 없다는 점이다.
1
2
3
4
5
try {
	User user = repository.findUserById(userId);
} catch (CheckedException e) {
	//...
}
  • findUserById() 메서드에서 체크 예외를 발생시켜 try-catch 블록으로 처리하거나 밖으로 던져주어야 하는 상황이라고 가정하자.
1
2
3
4
5
6
7
8
public Optional<User> findUserById(String userId) {
	try {
		return Optional.ofNullable(repository.findUserById(userId));
	} catch (SQLException e) {
		log.error("Database Error", e);
		return Optional.empty();
	}
}
  • 핵심은 예외를 밖으로 던지지 않아, 해당 API를 사용하는 사용자는 부담이 없어진다는 것이다.

검사 예외를 던지는 메서드를 2개로 쪼개 비검사 예외로 바꾼다.

  • 검사 예외를 던지는 메서드를 두 개로 쪼개어 비검사 예외로 바꾼다
  • 문제가 발생하는 지점을 boolean으로 받아 true일 경우에만 로직을 실행시키는 방법이다.
1
2
3
4
5
6
7
8
public boolean isExist(String userId) {
	try {
		repository.findUserById(userId);
		return true;
	} catch (SQLException e) {
		return false;
	}
}
1
2
3
4
5
6
7
8
9
10
11
public User findUserById(String userId) {
	if (!isExist(userId)) {
		throw new RuntimeException("유저가 존재하지 않습니다.");
	}

	try {
		return repository.findUserById(userId);
	} catch (SQLException e) {
		throw new RuntimeException("DataBase Error", e);
	}
}

이러한 리팩토링을 모든 상황에 적용할 수는 없지만, 적용할 수 있다면 쓰기 편한 API를 제공할 수 있다.

의존적이지 않고 보다 유연하다.

주의 사항

isExist() 메서드는 상태 검사 메서드에 해당한다.

아이템 69에서 상태 검사 메서드 대신 옵셔널이나 null을 반환하는 선택지가 있다고 했다.

이 경우는 외부 동기화 없이 여러 쓰레드가 동시에 접근할 수 있을 때 외부 요인에 의해 상태가 변할 수 있다면 상태 검사 메서드가 오동작할 수 있다.

따라서 이런 경우에는 위와 같은 리팩토링이 유효하지 않다. 또한 isExist()에서 작업의 일부를 대신 수행해주고 있는데 이 또한 성능에서 손해이다.

따라서 검사 예외를 피하려는 노력도 경우에 맞게 리팩토링해 대처해야 한다.

정리

검사 예외는 프로그램의 안전성을 높여줄 수 있다.

처음 자바 설계 당시 검사 예외가 더 나은 선택지라고 생각되기도 했다.

하지만 복구할 수 없는 예외가 많아지면서 오히려 검사 예외는 예외를 밖으로 던지는 행위를 통해 API 사용자의 부담감만 지우는 상태까지 왔다.

따라서 예외 상황에서 복구할 방법이 없는 경우에는 비검사 예외를 던지도록 하고, 복구가 가능하고 호출자가 그 처리를 해주기를 원하는 경우 검사 예외를 고민할 수 있다.

하지만 이 또한 검사 예외보다도 옵셔널을 우선적으로 고민하자. 상황을 처리하기에 충분한 정보를 제공할 수 없을 때 검사 예외를 사용하자.

This post is licensed under CC BY 4.0 by the author.

Item70 (복구할 수 있는 상황에서는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라)

Item72 (표준 예외를 사용하라) + 커스텀 예외에 대한 내 생각