자바 예외 이해하기
예외도 객체로 분류되기 때문에 다른 객체들과 마찬가지로 최상위 부모는 Object이다.
Throwable은 Object바로 아래의 최상위 예외로, Exception과 Error를 자손으로 가진다.
Error
메모리 부족이나 심각한 시스템 오류처럼 애플리케이션 레벨에서 복구 할 수 없는 시스템 예외이다.
에러가 발생하면 잡으려고 하지 말고 그냥 두자.
조상 에러를 catch로 잡으면 하위 예외도 함께 잡힌다. (던질 때도 마찬가지)
그러니 애플리케이션 로직에서 Error 예외를 잡지 않도록 하기 위해 Throwable 예외도 잡지 않도록 하자.
애플리케이션 로직은 Exception부터 필요한 예외로 생각하면 된다.
Exception
애플리케이션에서 잡을 수 있는 실질적인 최상위 예외이다.
Exception의 자손들 중 RuntimeException을 제외한 모든 자손들은 컴파일러가 체크해주는 체크 예외이다.
RuntimeException은 컴파일러가 체크하지 않는 언체크 예외이다.
런타임 예외라고도 부른다.
예외는 폭탄 돌리기이다.
발생 시 잡아서 처리하거나, 밖으로 던져야 한다.
Repository에서 예외가 발생했는데, Repository는 예외를 잡을 수 없다. 자신을 호출한 Service에게 예외를 던지자.
Service는 예외를 잡을 수 있다. 예외를 잡으면 애플리케이션 로직이 정상적으로 돌아온다.
예외를 아무도 처리하지 못하고 계속 던지다가 main 쓰레드에 던져졌으면, 예외 로그를 출력하고 시스템을 종료한다.
웹 애플리케이션일 경우 여러 사용자의 요청을 처리해야 하기 때문에 시스템을 종료하는 대신 WebApplicationServer가 해당 예외를 받아 클라이언트에게 개발자가 지정한 오류 페이지를 보여주는 방식으로 처리한다.
Exception을 상속받은 예외는 체크 예외로 간주하고, RuntimeException을 상속받은 예외는 언체크 예외로 간주한다. (자바 문법)
체크 예외를 잡아서 처리하려면 try - catch 문법을 사용하고, 처리할 수 없을 때는 <method() throws 예외 종류> 로 밖으로 던질 예외를 필수로 지정해 줘야 한다.
체크 예외는 예외를 잡아서 처리하지 못하면 항상 throws에 던지는 예외를 선언해야 하지만, 언체크 예외는 예외를 잡아 처리하지 않아도 throws를 생략할 수 있다. (자동으로 밖으로 던지고, 선언해도 상관 없긴 하다)
즉, 체크 예외와 언체크 예외는 예외를 처리할 수 없을 때 예외를 밖으로 던지는 부분을 필수로 선언해야 하는지와 생략해도 괜찮은지의 차이다.
그러면 언제 체크 예외를 사용하고 언제 언체크 예외를 사용할까?
기본 원칙을 기억하자.
1. 기본적으로는 언체크(런타임) 예외를 사용하자. (최근 경향)
2. 체크 예외는 비즈니스 로직에서 의도적으로 던지는 예외에서 사용하자.
해당 예외를 잡아서 반드시 처리해야 하는 부분에서 체크 예외를 사용한다.
어떤 예외를 체크 예외로 만들어서 해당 예외는 절대로 그냥 지나쳐서는 안된다고 강조한다고 생각하면 된다.
위와 같은 예시를 생각해보자.
리포지토리에서 SQLException, 네트워크 클라이언트에서 ConnectException이 발생했다.
해당 클래스를 호출한 서비스로 두 가지 예외가 모두 던져지고, 서비스도 예외를 처리할 수 없어 예외를 계속 던지는 상황이다.
이 때 두 가지 예외 모두 체크 예외이기 때문에 해당 클래스를 호출하는 모든 메서드에는 throws SQLException, ConnectionException 문장이 추가돼야 한다.
대부분의 예외는 복구가 불가능하다. 특히나 대부분의 서비스나 컨트롤러는 이런 문제를 해결할 수 없다.
웹 애플리케이션이라면 서블릿의 오류 페이지나 스프링 MVC의 ControllerAdvice에서 이런 예외들을 공통으로 처리하는데, 오류 로그를 남기고 개발자에게 해당 오류를 알려 주는 방식으로 작동한다.
여기서 문제되는 부분은 서비스나 컨트롤러에서 예외를 복구할 수 없다는 점과 throws 를 강제적으로 사용해 의존 관계가 꼬이는 점이다. (SQLException은 java.sql.SQLException 에 의존하는 등..)
추후 JDBC기술에서 JPA기술로 변경하게 되면 SQLException대신 JPAException을 처리해야 하는데, 이렇게 설계하면 SQLException 에 의존하던 모든 서비스, 컨트롤러의 코드를 JPAException 에 의존하도록 고쳐야 한다.
즉, 체크 예외를 사용해 아래에서 올라온 복구 불가능한 예외를 서비스와 컨트롤러같은 클래스들이 알아야 한다.
때문에 불필요한 의존관계가 발생해 OCP, DI 등 객체지향 설계의 원칙을 지킬 수 없게 된다.
(조상 타입인 Exception으로 예외를 던지면 문제를 해결하는 것 처럼 보이지만, 모든 예외를 다 던지게 돼 체크 예외를 의도대로 사용하지 못한다)
런타임 예외를 사용해보자.
SQLException, ConnectException을 런타임 예외로 바꿔서 사용해 서비스와 컨트롤러는 해당 예외를 처리할 수 없을 때 별도의 선언 없이 그대로 두고, 예외를 공통으로 처리하는 ControllerAdvice에서 예외를 처리한다.
시스템에서 발생한 예외는 대부분 복구가 불가능하다.
런타임 예외를 사용해 서비스나 컨트롤러가 예외를 신경쓰지 않도록 했다.
throw RuntimeSQLException, RuntimeConnectException을 생략할 수 있어 의존 관계가 꼬이지 않는다.
런타임 예외를 사용하면 필요한 경우 throw로 잡을 수 있고, 그 외의 경우 잡지 않고 내버려 둬 의존 관계를 유지할 수 있어 상황에 따라 유동적으로 사용할 수 있어 최근 라이브러리들은 대부분 런타임 예외를 기본으로 사용한다.
다만 런타임 예외는 놓칠 수 있으니 throw 런타임에러 혹은 주석을 통해 문서화 해 두는 편이 좋다.
예외를 전환할 때는 기존 예외를 꼭 포함해야 한다.
void printEx() {
Controller controller = new Controller();
try {
controller.request();
} catch (Exception e) {
//e.printStackTrace();
log.info("ex", e);
}
}
로그를 출력할 때 마지막 파라미터에 예외를 넣어주면 스택 트레이스에 로그를 출력할 수 있다.
public void call() {
try {
runSQL();
} catch (SQLException e) {
throw new RuntimeSQLException(e);
}
}
RuntimeSQLException(e); 에서 e 를 빼 버리면?
예외를 포함해주지 않아서 기존에 발생한 java.sql.SQLException과 스택 트레이스를 확인할 수 없다.
여기서 e에는 어떤 종류의 오류가 발생했는지 담겨있는데, 예외에 e를 넣지 않으면 어떤 예외가 발생했는지 알 수 없게 된다.
예외를 전환할 때 꼭 기존 예외를 포함해주자.
'Programming Language > Java' 카테고리의 다른 글
[Java] 컬렉션과 스트림 (0) | 2023.08.01 |
---|---|
[Java] 람다 표현식 (0) | 2023.06.09 |
[Java] 네트워킹 (Networking) 2 (0) | 2021.12.13 |
[Java] 네트워킹 (Networking) 1 (0) | 2021.12.05 |
[Java] 입출력 (I/O) 2 (0) | 2021.11.29 |
댓글
이 글 공유하기
다른 글
-
[Java] 컬렉션과 스트림
[Java] 컬렉션과 스트림
2023.08.01 -
[Java] 람다 표현식
[Java] 람다 표현식
2023.06.09 -
[Java] 네트워킹 (Networking) 2
[Java] 네트워킹 (Networking) 2
2021.12.13 -
[Java] 네트워킹 (Networking) 1
[Java] 네트워킹 (Networking) 1
2021.12.05