백기선 자바 스터디 9주차 예외 처리에 대해 학습하면서 정리한 내용입니다.
프로그램 실행 과정에서 의도하지 않은 상황이 발생할 수 있습니다. 존재하지 않는 파일을 열어보려하거나, 권한이 없는 파일에 데이터를 쓰려고 할 때 예외가 발생합니다. 이러한 예외 상황에서 프로그램을 종료하지 않고, 그에 맞는 적절한 처리를 하고 싶을 때가 있습니다. 예외 처리는 오류를 처리하여 프로그램의 흐름이 유지될 수 있도록 도와줍니다.
발생 시점에 따른 에러 분류
Compile-time Error: 컴파일러는 컴파일하는 시점에서 오타나 잘못된 구문, 자료형 등 검사를 수행합니다. 이 때 발생하는 에러를 컴파일 에러라고 합니다.
Runtime Error: 프로그램 실행 시점에서 발생하는 에러로 실행 도중 의도치 않은 동작에 의해 에러가 발생합니다.
예외와 오류의 차이
Runtime 시점에서 발생하는 오류는 예외(Exception)와 에러(error)로 나뉩니다. (+ 예외는 Compile-time에도 발생할 수 있습니다)
- 예외(Exception): 예외는 프로그램 실행 중 참조된 값이 잘못되거나 정상적인 프로그램의 흐름을 벗어나는 경우를 말합니다. 예외는 프로그램 코드로 처리할 수 있기 때문에 예외 상황을 미리 예측하여 핸들링할 수 있습니다. 예시로는 NullPointerException이 있습니다.
- 에러(Error): 에러는 시스템에 비정상적인 상황이 발생한 경우를 말합니다. 주로 JVM에서 발생하는 것이며 프로그램 코드로 처리할 수 없습니다. 에러의 예시로는 OutOfMemoryError, StackOverFlowError가 있습니다.
Logical Error: 논리적 오류는 소스 코드 컴파일도 정상적이고 런타임 에러 또한 발생하지 않지만 개발자의 의도와는 다르게 동작하는 에러를 말합니다.
예외 계층 구조
예외 처리하는 방법을 알아보기 전 예외 계층 구조에 대해 알아보도록 하겠습니다. 모든 예외와 에러 유형은 Throwable 클래스의 하위 클래스입니다.
- Exception class : 주로 외부의 영향으로 발생할 수 있는 것으로 프로그램의 사용자들의 동작에 의해 발생하는 예외에 사용됩니다. Exception은 Checked Exception과 Unchecked Exception 두 가지로 나눌 수 있습니다. CheckedException과 UncheckedException은 RuntimeException 상속 유무에 따라 구분합니다. Unchecked Exception은 RuntimeException을 상속하고 Checked Exception은 RuntimeException을 상속하지 않습니다.
- Checked Exception : 명시적인 예외 처리를 강제하기 때문에 Checked Exception이라고 합니다. 반드시 try catch로 예외를 잡거나 throw로 호출한 메서드에게 예외를 던져야 합니다.
- Checked Exception은 복구 가능한 예외이기 때문에 반드시 예외를 처리하는 코드를 작성해주어야 합니다. 해결하지 않았을 때는 컴파일 타임에 Checked Exception이 발생합니다.
- Unchecked Exception : 명시적인 예외 처리를 강제하지 않기 때문에 Unchecked Exception이라고 합니다. 명시적인 예외 처리란 throw로 호출한 메서드에게 예외를 던지지 않고 try catch로 예외를 잡지 않습니다.
- Unchecked Exception은 컴파일 타임에 확인되지 않습니다. 컴파일 타임에는 오류가 없지만 런타임 시점에 예외가 발생하게 됩니다.
- Checked Exception : 명시적인 예외 처리를 강제하기 때문에 Checked Exception이라고 합니다. 반드시 try catch로 예외를 잡거나 throw로 호출한 메서드에게 예외를 던져야 합니다.
- Error class : JVM에서 런타임 환경 자체(JRE)와 관련된 오류를 나타내는데 사용됩니다.
예외 종류
예외에는 두 가지 종류가 있습니다. 하나는 일반 예외, 다른 하나는 실행 예외입니다.
일반 예외는 컴파일러 체크 예외라고도 합니다. 일반 예외는 자바 소스 코드를 컴파일하는 과정에서 예외 처리 코드가 필요한지 컴파일러가 검사합니다. 이 과정에서 예외 처리 코드가 없다면 컴파일 오류가 발생합니다.
실행 예외는 컴파일하는 과정에서 예외 처리 코드를 검사하지 않는 예외를 말합니다.
일반 예외와 실행 예외는 컴파일시 예외 처리를 확인하는 차이일뿐 두 가지 모두 예외 처리가 필요합니다.
자바에서는 예외를 클래스로 관리합니다. JVM은 프로그램을 실행하는 도중 예외가 발생하면 해당 예외 클래스로 객체를 생성합니다. 그리고나서 예외 처리 코드에서 예외 객체를 이용할 수 있도록 해줍니다. 모든 예외 클래스는 java.lang.Exception 클래스를 상속 받습니다.
일반 예외는 Exception을 상속받지만 RuntimeException은 상속받지 않는 클래스입니다. 실행 예외는 RuntimeException을 상속 받는 클래스입니다. JVM은 RuntimeException을 상속했는지 여부를 확인하여 실행 예외를 판단합니다.
예외 처리 방법
예외 처리 방법에는 예외 복구, 예외 회피, 예외 전환이 있습니다.
예외 복구
예외가 발생하더라도 애플리케이션의 로직은 정상적으로 수행되게 하도록 처리하는 방법입니다.
int maxretry = MAX_RETRY;
while (maxretry-- > 0) {
try {
// 예외가 발생할 가능성이 있는 시도
return; // 작업 성공시 리턴
}
catch (SomeException e) {
// 로그 출력. 정해진 시간만큼 대기
}
finally {
// 리소스 반납 및 정리 작업
}
}
throw new RetryFailedException(); // 최대 재시도 횟수를 넘기면 직접 예외 발생
재시도를 통해 예외를 복구하는 코드 (출처 : 넥스트리소프트)
이 예제는 네트워크 환경이 좋지 않아 서버에 접속이 안 되는 상황의 시스템에 적용하면 효율적입니다. 예외가 발생하면 그 예외를 잡아 일정 시간만큼 대기하고 다시 재시도를 반복합니다. 최대 재시도 횟수를 넘기면 예외를 발생시킵니다. 재시도를 통해 정상적인 흐름으로 수행하게 하거나, 예외가 발생하면 이를 미리 예측하여 다른 흐름으로 유도시키도록 구현하여 정상적으로 작업을 종료할 수 있습니다.
예외 회피
예외 처리를 직접 처리하지 않고 호출한 메서드에게 예외 처리를 위임하는 방법입니다. 코드적으로는 깔끔하게 예외 처리가 되지만 역할을 분담하고 있는 관계가 아닐 경우 예외를 던지는 것은 무책임한 방법입니다.
public void add() throws SQLException {
... // 구현 로직
}
예외처리 회피 (출처 : 넥스트리소프트)
예외가 발생하면 throws를 통해 호출한 쪽으로 예외를 던지고 그 처리를 회피하고 있습니다. 하지만 무책임하게 던지는 것은 위험합니다. 호출한 쪽에서 다시 예외를 받아 처리하도록 하거나, 해당 메서드에서 이 예외를 던지는 것이 최선의 방법이라는 확신이 있을 때 사용해야 합니다.
예외 전환
예외 처리 회피와 비슷하게 메서드 밖으로 예외를 던집니다. 하지만 그냥 던지는 게 아닌 적절한 예외로 전환하여 던지는 방법입니다. 명확한 의미로 전달되기 위해 적합한 의미를 가진 예외로 변경하여야 합니다.
catch(SQLException e) {
...
throw DuplicateUserIdException();
}
예외 전환을 위한 중첩 예외 (출처 : 넥스트리소프트)
예외 전환은 예외를 잡아서 다른 예외를 던집니다. 호출한 쪽에서 예외를 받아서 처리할 때 명확하게 인지할 수 있도록 돕기 위한 방법입니다. 어떤 예외인지 분명해야 처리가 수월해지기 때문입니다. 예로 Checked Exception 중 복구 불가능한 예외가 잡혔다면 이를 Unchecked Exception으로 전환하여 다른 계층에서 예외를 선언할 필요가 없도록 할 수 있습니다.
커스텀 예외(Custom Exception)
표준 예외와 예외 메세지만으로도 충분히 오류 상황을 표현할 수 있습니다. 하지만 상세한 예외 정보를 제공할 수 없기 때문에 여러 곳에서 같은 예외를 발생시키고 있다면 예외가 발생한 곳을 디버깅을 통해 찾아야하는 어려움이 있습니다.
이런 상황에서 커스텀 예외(사용자 정의 예외)는 좋은 해결책이 될 수 있습니다.
커스텀 예외 만드는 방법
사용자 정의 클래스를 만들 때에는 일반 예외를 선언할 수 있고, 실행 예외를 선언할 수도 있습니다.
- 일반 예외는 Exception, 실행 예외는 RuntimeException을 상속받아 구현합니다.
- 사용자 정의 클래스 작성시 생성자는 두 개를 선언하는 것이 일반적입니다.
- 매개 변수가 없는 기본 생성자
- 예외 메세지를 전달하기 위해 String 타입의 매개변수를 갖는 생성자
if (index >= arr.length) {
throw new IndexOutOfBoundsException("범위를 벗어났습니다.");
}
표준 예외를 이용한 예시 (출처 : tecoble, custom exception을 언제 써야 할까)
표준 예외를 이용하여 예외 메세지를 전달하고 있습니다. 범위를 벗어났다는 건 알겠으나 전체 범위가 얼마인지, 요청한 index가 몇인지 파악하기 위해서는 직접 디버깅하거나 정보를 담은 메세지를 만들어줘야합니다.
이런 상황에서 사용자 정의 예외는 좋은 해결책이 될 수 있습니다.
public class IllegalIndexException extends IndexOutOfBoundsException {
private static final String message = "범위를 벗어났습니다.";
public IllegalIndexException(List<?> target, int index) {
super(meesage + " size: " + target.size() + " index: " + index);
}
}
표준 예외 IndexOutOfBoundsException 메서드를 상속 받아 만든 사용자 정의 예외 클래스 (출처 : tecoble, custom exception을 언제 써야 할까)
사용자 정의 예외 클래스를 통해 요청 받은 컬레션의 최대 범위가 어디까지인지, 요청한 index는 몇인지 바로 알 수 있습니다. 전달하고자 하는 정보의 수정이 필요할 때는 IllegalIndexException 클래스를 수정하면 됩니다. 같은 예외를 발생시키는 모든 상황에 적용되어 예외에 대한 응집도 향상될 수 있습니다.
마무리
끝으로 이번 포스팅의 키워드를 짚어보고 마무리하도록 하겠습니다.
예외(Exception)
예외는 프로그램 실행 중 참조된 값이 잘못되거나 정상적인 프로그램의 흐름을 벗어나는 경우를 말합니다. 예외는 프로그램 코드로 처리할 수 있기 때문에 예외 상황을 미리 예측하여 핸들링할 수 있습니다.
에러(Error)
에러는 시스템에 비정상적인 상황이 발생한 경우를 말합니다. 주로 JVM에서 발생하는 것이며 프로그램 코드로 처리할 수 없습니다. 에러의 예시로는 OutOfMemoryError, StackOverFlowError가 있습니다.
Checked Exception
Checked Exception은 Exception 클래스는 상속 받지만 RuntimeException 클래스는 상속받지 않습니다. 컴파일러가 컴파일 시점에서 명시적인 예외 처리를 해주었는지 확인합니다.
Unchecked Exception
Unchecked Exception은 RuntimeException 클래스를 상속 받은 클래스입니다. 컴파일러가 컴파일 시점에 RuntimeException 클래스를 상속 받은 클래스는 명시적인 예외 처리를 확인하지 않기 때문에 명시적인 예외 처리는 필요하지 않습니다.
예외 처리 방식
예외 처리 방식에는 예외 복구, 예외 회피, 예외 전환이 있습니다. 예외 복구 방식은 예외 상황을 파악하고 문제를 해결하여 정상 상태로 복구합니다. 예외 회피 방식은 예외 처리를 직접 담당하지 않고 호출한 쪽으로 예외 처리를 회피합니다. 예외 전환 방식은 적절한 예외로 전환하여 명확한 의미로 전달하기 위하여 적합한 의미의 예외로 변경하여야 합니다.
올바른 예외 처리 방식
예외 복구 전략이 명확하고 복구가 가능하다면 Checked Exception을 try catch로 잡아 예외를 복구하는 것이 좋습니다. 복구가 불가능한 Checked Exception이 발생하면 더 구체적인 Unchecked Exception을 발생시키고 예외에 대한 메세지를 명확하게 전달하도록 합니다. 예외를 무책임하게 상위 메서드에 throw로 던지는 것은 상위 메서드의 책임을 증가시키기 때문에 좋지 않은 방법입니다. (출처 : 느리더라도 꾸준하게
커스텀 예외(사용자 정의 예외)를 사용하는 이유
표준 예외와 예외 메세지를 통해오류 상황을 표현할 수 있습니다. 하지만 여러 곳에서 같은 예외를 발생시키고 있다면 예외 발생시 예외를 발생시킨 곳을 찾기 위해 디버깅을 해야하는 어려움이 있습니다. 이 때 사용자 정의 예외를 사용이 좋은 해결책이 될 수 있습니다. 사용자 정의 예외를 사용함으로써 클래스 이름으로 예외 상황을 유추할 수 있고, 상세한 예외 정보를 제공할 수 있으며 예외에 대한 응집도가 향상될 수 있습니다.
References
https://devlog-wjdrbs96.tistory.com/141
https://ko.gadget-info.com/difference-between-error
https://sabarada.tistory.com/76
https://www.nextree.co.kr/p3239/
https://tecoble.techcourse.co.kr/post/2020-08-17-custom-exception/
예외처리 잘 하는 방법
https://soft.plusblog.co.kr/160
'Java' 카테고리의 다른 글
enum (0) | 2022.10.07 |
---|---|
멀티스레드 프로그래밍 (2) | 2022.09.27 |
Java의 Dynamic Method Dispatch, Abstract class, final, Object class (0) | 2022.09.01 |
Java Overriding (0) | 2022.08.30 |
Java의 상속과 super 키워드 (0) | 2022.08.29 |