Java
[자바스터디 9주차] 예외 처리
임요환
2023. 6. 22. 11:17
목표
- 자바의 예외 처리에 대해 학습하세요
학습할 것 (필수)
- 자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
- 자바가 제공하는 예외 계층 구조
- Exception과 Error의 차이는?
- RuntimeException과 RE가 아닌 것의 차이는?
- 커스텀한 예외 만드는 방법
자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
자바의 예외처리
- 프로그램 실행 중에 발생할 수 있는 예상치 못한 상황을 나타내는 객체
- 이러한 예외상황을 적절히 처리하고 프로그램의 안정성을 높이기 위해 중요함
try {
// 예외가 발생할 수 있는 코드
} catch (ExceptionType1 e1) {
// 예외 처리 코드
} catch (ExceptionType2 e2) {
// 예외 처리 코드
} finally {
// 선택적인 finally 블록 (필요한 경우 사용)
}
- try 문 = 예외가 발생할 수 있는 코드를 작성
- catch 문 = 해당 예외를 처리하는 코드를 작성
- finally 문 = 선택적으로 작성하나 주로 자원의 반납에 사용
public void throwException() throws Exception {
throw new Exception("error");
}
- throw
- 예외를 명시적으로 발생시킬 때 사용
- 예외가 발생하는 상황을 감지하고 예외 객체를 생성하여 해당 예외를 처리할 코드 블록으로 전달
- throw 키워드가 try-catch 문 내부에서 사용되면 예외 발생 시 catch문으로 제어를 이동시킴
- throws
- 메서드 선언부에 사용되며 메서드가 호출될 때 발생할 수 있는 예외를 명시적으로 선언함
- 메서드에서 발생할 수 있는 예외를 호출자에게 알려줌으로써 예외 처리를 해당 메서드를 호출한 곳에서 처리할 수 있도록 함
- 메서드 시그니처 뒤에 예외 클래스들을 선언하고 여러 개의 예외를 선언할 경우 쉼표로 구분
- throws로 선언된 예외는 직접 try-catch 문을 사용하여 처리하거나 상위 호출자에게 예외를 전달할 수 있음
catch 문 여러개일 시 순서의 중요성
try {
// do something
} catch (RuntimeException e){
} catch (IllegalArgumentException e) { // 무의미한 코드가 되어버림
}
- 여러 줄 catch 할 때 주의해야 할 사항
- 예외 계층 구조의 상속관계 때문에 순서가 중요함
- IllegalArgumentException은 RuntimeException을 상속받은 관계이므로 IllegalArgumentException에 도달하기 전에 이미 RuntimeException에서 예외가 잡혀버림
try {
// do something
} catch (IllegalArgumentException e){
} catch (RuntimeException e) {
}
- 좁은 범위의 예외를 위에서 catch 해야 함
- try 구문에서 호출하는 메서드가 어떤 예외를 던지느냐에 따라 catch 하는 예외와 순서가 달라짐
multi-catch
try {
// do something
} catch (IllegalArgumentException e){
} catch (IllegalStateException e) {
}
try {
// do something
throw new IllegalArgumentException();
} catch (IllegalArgumentException | IllegalStateException e){
System.out.println(e.getClass()); // class java.lang.IllegalArgumentException
}
- 자바 1.7 이상부터 두 개의 예외의 처리가 같다면 하나로 합칠 수 있음
- catch 한 e에 들어오는 클래스는 try 구문에서 어떤 예외를 던지느냐에 따라 결정됨
try {
// do something
} catch (RuntimeException | IllegalStateException e){ // 순서 바꿔도 컴파일 에러
}
- multi-catch 하는 예외가 상속관계면 multi-catch 불가능
예외 처리 비용
- 예외 처리는 StackTrace에 내용이 계속 담겨 메모리를 사용하기 때문에 비용이 비쌈
try-with-resources
FileReader reader = null;
try {
reader = new FileReader("file.txt");
// 파일 읽기 작업 수행
// 예외가 발생할 수 있는 코드
} catch (IOException e) {
// 예외 처리 코드
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// 자원 해제 실패에 대한 예외 처리
}
}
}
try (FileReader reader = new FileReader("file.txt")) {
// 파일 읽기 작업 수행
System.out.println("do");
// 예외가 발생할 수 있는 코드
} catch (IOException e) {
// 예외 처리 코드
} finally {
// finally 구문도 추가적으로 사용 가능 -> try with resources는 finally 블록에서 close 로직을 생성하는게 아니라 catch 블록에서 close하는 로직을 생성해주고 또 맨 밑에 close하는 로직을 추가로 생성해줌
}
// try-with-resources를 컴파일한 class파일
try{
FileReader reader = new FileReader("file.txt");
try {
System.out.println("do");
} catch (Throwable var5) {
try {
reader.close();
} catch (Throwable var4) {
var5.addSuppressed(var4);
}
throw var5;
}
reader.close();
} finally {
//finally 추가시 try로 한번더 감싸져서 finally가 추가됨
}
- 예외처리 메커니즘을 활용하여 자원관리를 간편하게 해주는 기능(finally 블록을 줄여줌)
- try 안에 생성하는 자원은
AutoCloseable
인터페이스를 구현한 클래스이어야만 함 - AutoCloseable의 close메서드를 명시적으로 호출하므로 자동으로 자원을 정리함
- 코드의 가독성과 안정성을 향상하고, 자원 누수를 방지함
public class JavaPuzzler40 {
static void copy(String src, String dest) throws IOException {
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream(src);
out = new FileOutputStream(dest);
byte[] buf = new byte[1024];
int n;
while((n = in.read(buf)) >= 0){
out.write(buf, 0, n);
}
} finally {
if(in != null) {
in.close(); // in.close하는 부분도 try catch로 잡아주어야 다음 out.close하는 부분으로 내려가 자원을 종료할 수 있음, 만약 에러가 던져지면 out의 자원은 종료되지 못하고 자원 누수가 발생됨
}
if(out != null) {
out.close();
}
}
}
}
finally 안에 return문(안티패턴)
public class FinallyReturnTest {
public static void main(String[] args) {
System.out.println(new FinallyReturnTest().returnTest()); // 무조건 finally에서 리턴된 값이 나옴
}
public String returnTest() {
try {
System.out.println("try");
// throw new IllegalStateException("error"); // 에러발생시에는 catch 값까지 print한 후 finally 값이 리턴됨
return "try";
} catch (Exception e) {
System.out.println("catch");
return "catch";
} finally {
System.out.println("finally");
// throw new IllegalStateException("error"); // 이런식으로도 사용 x
return "finally"; // 이런식으로도 사용 x
}
}
}
- try 문 return = finally 블록을 거쳐 정상 실행
- catch 문 return = finally 블록을 거쳐 정상 실행
- finally 문 return = try 블록 안에서 발생한 예외 무시되고 finally 거쳐 정상 종료(예외를 알 수 없음)
- finally에서는 코드의 흐름을 제어하는 로직은 작성하지 않는 게 좋음
- finally에서는 자원 반납 혹은 로깅정도만 하는 형식으로 사용하는 것을 권장함
자바가 제공하는 예외 계층 구조
- java.lang.Throwable
- 모든 예외 클래스의 최상위 클래스
- 예외와 에러의 기본 클래스 = Exception과 Error 클래스의 부모 클래스
- java.lang.Exception
- 일반적인 예외 상황을 나타내는 클래스
- 다양한 예외 하위 클래스를 가짐
- Exception 클래스를 상속받은 예외들은 예외처리가 필요한 Checked Exception임
- java.lang.RuntimeException
- 프로그램 오류가 아닌 주로 개발자의 잘못된 사용으로 발생하는 예외를 나타냄
- RuntimeException을 상속받은 예외들은 Unchecked Exception으로 예외처리가 선택적임
- Exception 클래스의 자식 클래스임
- java.lang.Error
- 시스템 레벨에서 발생하는 심각한 예외 상황을 나타내는 클래스
- Error 클래스를 상속받은 예외들은 프로그램에서 복구가 불가능하며 주로 가상 머신에서 발생하는 문제를 나타냄
Exception과 Error의 차이는?
Exception | Error |
---|---|
프로그램 실행 중에 예상할 수 있는 예외 처리 | 시스템 레벨에서 발생하는 심각한 예외 상황을 나타냄, 가상머신 또는 런타임 환경에서 발생하는 문제 |
Exception 클래스를 상속받은 예외는 예외처리가 필요한 Checked Exception이므로 반드시 예외처리 코드를 작성해야함 | Error를 상속받은 예외들은 프로그램에서 복구가 불가능함 |
잘못된 입력, 파일을 찾을수 없음, 네트워크 연결 오류 등과 같이 예상할 수 있는 예외 상황에 대응하기 위해 사용 | 예외처리 코드를 작성하여 예외를 처리하는 것은 권장되지 않으며 시스템 레벨에서 처리되야함 |
RuntimeException, IOException, SQLException | OutOfMemoryError, StackOverflowError, NoClassDefFoundError, AssertionError |
RuntimeException과 RE가 아닌 것의 차이는?
- RuntimeException(UncheckedException)
- Exception 클래스의 하위클래스로 주로 개발자의 잘못된 사용으로 발생하는 예외
- 예외 처리가 선택적이므로 컴파일러가 예외처리 코드를 강제하지 않음
- 예외를 코드로 복구하기 힘든 경우
- 컴파일러가 확인할 수 없는 예외
- 프로그램의 코드 논리나 인수 유효성 검사와 관련된 문제를 나타내며 개발자의 코드 수정으로 예방 가능한 예외 상황들임
- CheckedExcpetion
- 예외처리가 필수적이며 컴파일러가 예외처리 코드를 요구함
- 예외가 났을 때 코드로 컨트롤이 가능할 경우(재시도, 값 수정 등)
- 컴파일러가 확인할 수 있는 예외
- 예외 처리 코드 필요
- 외부 리소스와의 상호작용(파일, 네트워크, 데이터베이스 등)이나 예외적인 상황에 대응하기 위한 예외 상황
- 예외처리 코드를 통해 예외 상황에 대한 안정성과 오류 복구를 제공함
자바가 제공하는 RuntimeException 기본 예외
예외 | 설명 |
---|---|
NullPointerException | null 참조에 대한 연산이 수행될 때 발생 |
IllegalArgumentException | 잘못된 인수가 메서드에 전달될 때 발생 |
IndexOutOfBoundsException | 배열 또는 컬렉션의 인덱스가 범위를 벗어날 때 발생 |
ArithmeticException | 산술 연산 중에 발생하는 예외를 처리하기 위한 기본 클래스 |
ClassCastException | 잘못된 형 변환을 시도할 때 발생 |
UnsupportedOperationException | 지원되지 않는 작업을 수행하려고 할 때 발생 |
IllegalStateException | 객체의 상태가 메서드의 호출에 적합하지 않을 때 발생 |
NumberFormatException | 숫자로 변환할 수 없는 문자열을 숫자로 변환하려고 할 때 발생 |
커스텀한 예외 만드는 방법
public class CustomException extends RuntimeException {// or Exception 상속
public CustomException(String message, Throwable cause) {
super(message, cause);
}
}
try {
// do something
} catch (Exception e) {
throw new CustomException("message", e); // root cause(= e) 보내기
}
- 예외 클래스 작성
- 커스텀한 예외 클래스를 작성하기 위한 새로운 클래스 생성
- Exception 또는 RuntimeException을 상속받음
- 예외 클래스의 이름은 보통 Exception으로 끝나는 것이 관례임
- 생성자 작성
- 예외 클래스에는 일반적으로 기본 생성자와 메시지를 받는 생성자를 구현
- 메시지를 받는 생성자는 예외에 대한 추가 정보를 전달할 수 있도록 해야 함
- 추가적인 메서드 및 속성 작성
- 필요에 따라 예외 클래스에 추가적인 메서드와 속성을 작성할 수 있음
- 커스텀한 예외에 특정한 동작이나 데이터를 추가하고자 할 때 유용함
- 예외 발생
- 예외를 발생시키기 위해 throw 키워드를 사용하여 해당 예외 객체를 생성하고 던짐
- 예외 상황이 발생했을 때 예외 객체를 생성하여 예외 처리 코드로 전달
커스텀 예외를 만들 때 참고해야 할 사항
- Always Provide a Benefit
자바의 표준 예외들은 다양한 장점을 가지는 기능들이 있으므로 최대한 활용하고 어떠한 장점도 가지지 못하는 커스텀 예외는 만들지 않는 것이 좋음 - Follow the Naming Convention
자바의 예외 클래스들은 모두 Exception으로 끝나므로 커스텀 예외를 만들 시 규칙을 따르는 것이 좋음 - Provide javadoc Comments for Your Exception Class
기본적으로 API의 모든 클래스, 멤버변수, 생성자들에 대해 문서화하는 것이 좋음, JavaDoc과 문서화를 하는 게 다른 개발자들이 목적을 이해하기 좋음 - Provide a Constructor That Sets the Cause
생성자에 Catch 한 예외를 넘겨줄 수 있게 만들어라, root cause(근본적인 에러 원인)를 꼭 보내라