자바는 프로그램 실행 중에 발생할 수 있는 예상치 못한 상황, 즉 예외(Exception)를 처리하기 위한 메커니즘을 제공한다. 이는 프로그램의 안정성과 신뢰성을 높이는 데 중요한 역할을 한다.
자바의 예외 처리는 다음 키워드를 사용한다:
`try`, `catch`, `finally`, `throw`, `throws`
그리고 예외를 다루기 위한 예외 처리용 객체들을 제공한다.
- `Object`: 자바에서 기본형을 제외한 모든 것은 객체이기 때문에 예외도 객체이다. 모든 객체의 부모는 `Object`이므로 예외의 최상위 부모도 `Object`이다.
- `Throwable`: 최상위 예외이다. 하위에 `Exception`과 `Error`가 있다.
- `Error`: 메모리 부족이나 심각한 시스템 오류와 같이 애플리케이션에서 복구가 불가능한 시스템 예외이다. 애플리케이션 개발자는 이 예외를 잡으려고 해서는 안된다.
- `Exception`: 체크 예외
- 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다.
- `Exception`과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다. 단 `RuntimeException`은 예외로 한다.
- `RuntimeException`: 언체크 예외
- 컴파일러가 체크하지 않는 언체크 예외이다.
- `RuntimeException`과 그 자식 예외는 모두 언체크 예외이다.
- `RuntimeException`의 이름을 따라서 `RuntimeException`과 그 하위 언체크 예외를 런타임 예외라고 많이 부른다.
체크 예외 vs 언체크 예외
체크 예외는 발생한 예외를 개발자가 명시적으로 처리해야 한다(명시적으로 던지거나 잡거나). 그렇지 않으면 컴파일 오류가 발생한다.
언체크 예외는 개발자가 발생한 예외를 명시적으로 처리하지 않아도 된다.
‼️ 주의 ‼️
상속 관계에서 부모 타입은 자식을 담을 수 있다. 이 개념이 예외 처리에도 적용되는데, 상위 예외를 `catch`로 잡으면 그 하위 예외까지 함께 잡는다. 따라서 애플리케이션 로직에서는 `Throwable` 예외를 잡으면 안되는데, 앞서 이야기한 잡으면 안되는 `Error`예외도 함께 잡을 수 있기 때문이다. 애플리케이션 로직은 이런 이유로 `Exception`부터 필요한 예외로 생각하고 잡으면 된다.
자바 예외 처리 기본 규칙
예외는 2가지 기본 규칙을 따른다
1. 예외는 잡아서 처리하거나 밖으로 던저야 한다.
2. 예외를 잡거나 던질 때 지정한 예외뿐만 아니라 그 예외의 자식들도 함께 처리할 수 있다.
- 예를 들어서 `Exception`을 `catch`로 잡으면 그 하위 예외들도 모두 잡을 수 있다.
- `Exception`을 `throws`로 던지면 그 하위 예외들도 모두 던질 수 있다.
// 체크 예외는 잡아서 처리하거나, 던지거나 하나를 필수로 선택해야 한다.
// 예외를 잡아서 처리하는 코드
public void callCatch() {
try {
client.call();
} catch (MyCheckedException e) {
//예외 처리 로직
System.out.println("예외 처리, message=" + e.getMessage());
}
System.out.println("정상 흐름");
}
- 예외를 잡아서 처리하려면 `try ~ catch(..)`를 사용해서 예외를 잡으면 된다.
- `try`코드 블록에서 발생하는 예외를 잡아서 `catch`로 넘긴다.
- 만약 `try`에서 잡은 예외가 `catch`의 대상에 없으면 예외를 잡을 수 없다. 이때는 예외를 밖으로 던져야 한다.
예외를 처리하지 못하고 계속 던지면 어떻게 될까?
- 자바 `main()` 밖으로 예외를 던지면 예외 로그를 출력하면서 시스템이 종료된다.
체크 예외
package exception.basic.checked;
// Exception을 상속받은 예외는 체크 예외가 된다.
public class MyCheckedException extends Exception {
public MyCheckedException(String message) {
super(message);
}
}
- 예외 클래스를 만들려면 예외를 상속받으면 된다.
- `Exception`을 상속받은 예외는 체크 예외가 된다.
- `super(message)`로 전달한 메시지는 `Throwable`에 있는 `detailMessage`에 보관된다.
- `getMessage()`를 통해 조회할 수 있다.
package exception.basic.checked;
public class Client {
public void call() throws MyCheckedException {
throw new MyCheckedException("ex");
}
}
- `throw 예외`라고 하면 새로운 예외를 발생시킬 수 있다. 예외도 객체이기 때문에 객체를 먼저 `new`로 생성하고 예외를 발생시켜야 한다.
- `throws 예외`는 발생시킨 예외를 메서드 밖으로 던질 때 사용하는 키워드이다.
- 체크 예외는 `throws`를 지정하지 않으면 컴파일 오류가 발생한다.
- `throw`, `throws`의 차이에 주의하자!!!
- 체크 예외는 `try ~ catch`로 잡아서 처리하거나 `throws`를 지정해서 예외를 밖으로 던진다는 선언을 필수로 해줘야 한다.
체크 예외의 장단점
장점: 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 안전장치 역할을 한다. 이를 통해 개발자는 어떤 체크 예외가 발생하는지 쉽게 파악할 수 있다.
단점: 개발자가 모든 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에, 너무 번거로운 일이 된다. 크게 신경 쓰고 싶지 않은 예외까지 모두 챙겨야 한다
언체크 예외
package exception.basic.unchecked;
// RuntimeException을 상속받은 예외는 언체크 예외가 된다.
public class MyUncheckedException extends RuntimeException {
public MyUncheckedException(String message) {
super(message);
}
}
- 언체크 예외는 컴파일러가 예외를 체크하지 않는다는 뜻이다
- 체크 예외와 기본적으로 동일하지만 예외를 던지는 `throws`를 선언하지 않고, 생략할 수 있다. 생략한 경우 자동으로 예외를 던진다
- 필요한 경우 선택적으로 `try ~ catch`문을 사용해 예외를 잡아서 처리할 수 있다.
언체크 예외의 장단점
장점: 신경 쓰고 싶지 않은 언체크 예외를 무시할 수 있다. 체크 예외의 경우 처리할 수 없는 예외를 밖으로 던지려면 항상 `throws 예외`를 선언해야 하지만, 언체크 예외는 이 부분을 생략할 수 있다.
단점: 언체크 예외는 개발자가 실수로 예외를 누락할 수 있다. 반면에 체크 예외는 컴파일러를 통해 예외 누락을 잡아준다.
체크 예외 vs 언체크 예외
그렇다면 체크, 언체크 예외를 어떻게 사용하면 되는 것인가?... 나눠놓은 이유가 있을 텐데 말이다.
결론부터 말하자면 현대 개발에서는 실용적으로 체크 예외를 거의 사용하지 않는다.
그렇다면 이유를 알아보자
체크 예외 사용의 부담
- 실무에서는 수많은 라이브러리를 사용하고, 또 다양한 외부 시스템과 연동한다.
- 사용하는 각각의 클래스들이 자신만의 예외를 모두 체크 예외로 만들어서 전달한다고 가정하자.
- 시스템 오류 때문에 발생한 예들은 대부분 예외를 잡아도 해결할 수 있는 것이 거의 없다.
- 때문에 예외를 밖으로 던저야만 한다..
- 결국엔 이렇게 모든 체크 예외를 하나씩 다 밖으로 던저야 하는 예외 처리 지옥이 펼쳐진다...
- 라이브러리가 늘어날수록 다루어야 하는 예외도 더 많아진다. 개발자 입장에서 이것은 상당히 번거로운 일이 된다.
그렇다면 예외를 하나씩 처리하지 말고 최상위 타입인 `Exception`을 던지면 되지 않나요???
응 안된다. `Exception`은 최상위 타입이므로 모드 체크 예외를 다 밖으로 던지는 문제가 발생한다.
결과적으로 다른 체크 예외를 체크할 수 있는 기능이 무효화되고, 중요한 체크 예외를 다 놓치게 된다. 중간에 중요한 체크 예외가 발생해도 컴파일러는 `Exception`을 던지기 때문에 문법에 맞다고 판단해서 오류가 발생하지 않는다.
이렇게 하면 체크 예외를 의도한 대로 사용하는 것이 아니다. 따라서 꼭 필요한 경우가 아니면 이렇게 `Exception` 자체를 밖으로 던지는 것은 좋지 않은 방법이다.
그렇다면 이제 언체크 예외를 사용해서 예외 처리 지옥에서 벗어나보자
언체크 예외를 사용한 예외 공통 처리
이렇게 처리할 수 없는 예외들은 중간에 여러 곳에서 나누어 처리하기보다는 예외를 공통으로 처리할 수 있는 곳을 만들어서 한 곳에서 해결하면 된다. 어차피 해결할 수 없는 예외들이기 때문에 이런 경우 고객에게는 현재 시스템에 문제가 있습니다.라고 오류 메시지를 보여주고, 만약 웹이라면 오류 페이지를 보여주면 된다. 그리고 내부 개발자가 지금의 문제 상황을 빠르게 인지할 수 있도록, 오류에 대한 로그를 남겨두면 된다. 이런 부분은 공통 처리가 가능하다.
스프링이나 JPA 같은 기술들도 대부분 언체크 예외를 사용한다. 언체크 예외도 필요하면 잡을 수 있기 때문에 필요한 경우에는 잡아서 처리하고, 그렇지 않으면 자연스럽게 던지도록 둔다. 그리고 처리할 수 없는 예외는 공통으로 처리하는 부분을 만들어서 해결하면 된다.
'Java' 카테고리의 다른 글
[Java, 제네릭] 제네릭 정리 (0) | 2024.11.01 |
---|---|
[Java, 예외처리] 예외처리 활용 방법 (1) | 2024.10.29 |
[Java, Object] Object 클래스 정리 (0) | 2024.10.26 |
[Java, 다형성] 다형성 정리2 - 추상 클래스, 인터페이스 (1) | 2024.10.25 |
[Java, 다형성] 다형성 정리 1 (0) | 2024.10.24 |