예외 처리 (Exceptions, Throws, Try-catch)

초코칩
2023년 12월 04일 17:20

프로그램 오류
프로그램 오류란 어떤 원인에 의해 오작동 하거나 비정상적으로 종료되는 경우를 의미한다.
종류
- 컴파일 에러: 컴파일 시에 발생하는 에러
- 런타임 에러: 실행 시에 발생하는 에러
- 논리적 에러: 실행은 되지만, 의도와 다르게 동작하는 에러
런타임 에러를 방지하기 위해 프로그램의 실행도중 발생할 수 있는 모든 경우의 수를 고려하여 이에 대한 대비가 필요하다. 자바에서는 실행 시(runtime)에 발생할 수 있는 프로그램 오류를 **에러(Error)와 예외(Exception)**로 구분했다.
- 에러(Error): 프로그램 코드에 의해서 수습될 수 없는 심각한 오류로 종료해야 한다.
- 예외(Exception): 프로그램 코드에 의해서 수습될 수 있는 미약한 오류이다.
예외 클래스의 계층 구조
Object 클래스로부터 오류(에러와 예외) 클래스들을 정의하였다.
Throwable 클래스 메서드
getMessage()
return String: 발생한 예외클래스의 인스턴스에 저장된 메세지를 확인할 수 있다.printStackTrace()
return void: 예외발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.getStackTrace()
return StackTraceElement: printStackTrace()에 해당하는 정보들을 반환한다.
printStackTrace()
지양해야 하는 이유
- System.err 로 쓰여지게 되면서 제어하기 힘들다.
- System.err 로 쓰여지게 되면서 리소스 비용이 비싼편이다.
- Java의 Reflection(리플렉션)을 사용하여 예외를 추적하는 것이라 많은 오버헤드가 발생한다.
- 서버에서 메소드(Method) 스택정보를 취합하기 때문에 서버에 부하의 원인이 된다.
- 출력이 어디로 향하는지 파악하기 어렵다.(톰캣의 경우 catalina.out 에 남을 수도 있다)
- 관리가 어렵다.(보통 log4j, logback 으로 로그 패턴 및 로그 메세지를 지정하여 사용한다.)
Exception
클래스를 두 가지로 나눌 수 있다.
- Exception클래스들: Exception 클래스와 그 자손들
- RunTimeException클래스들: RuntimeException 클래스와 그 자손들
Exception클래스들 (Checked Exception)
주로 외부의 영향으로 발생할 수 있는 예외들이다. 프로그램의 사용자 동작에 의해서 발생하는 경우가 많다.
- 존재하지 않는 파일의 이름 입력(
FileNotFoundException
) - 실수로 클래스의 이름 잘못 입력(
ClassCastException
) - 잘못된 입력 데이터 형식(
DataFormatException
)
RunTimeException클래스들 (Unchecked Exception)
주로 프로그래머의 실수에 의해서 발생될 수 있는 예외들로 자바 프로그래밍 요소들과 관계가 깊다.
- 배열의 유효하지 않는 범위(
IndexOutOfBoundsException
) - 참조변수가 null인 멤버 호출(
NullPointerException
) - 잘못된 클래스 형변환(
ClassCastException
) - 0으로 나누기(
ArithmeticException
)
사용자 정의 예외 - Checked vs Unchecked Exception
Checked Exception
- Checked Exception 또는 Compile Time Exception 이라고 한다.
- 컴파일 시점에 Exception을 catch하는지 확인한다. 컴파일 시점에 Exception에 대한 처리(try/catch)를 하지 않을 경우 컴파일 에러가 발생한다.
- Exception이 발생하는 메소드에서 throws 예약어를 활용해 Exception을 호출 메소드에 전달해야 한다.
- RuntimeException 제외 모든 Exception(ex. IOException)
Unchecked Exception
- Unchecked Exception 또는 RuntTime Exception 이라고 한다.
- 컴파일 시점에 Exception을 catch하는지 확인하지 않는다. 컴파일 시점에 Exception이 발생할 것인지의 여부를 판단할 수 없다.
- Exception이 발생하는 메소드에서 throws 예약어를 활용해 Exception을 처리할 필요가 없다. 하지만 처리해도 무방하다.
Checked Exception과 Unchecked Exception 선택 방법
- 호출하는 메소드가 Exception을 활용해 무엇인가 의미 있는 작업을 할 수 있다면 Checked Exception을 사용하라.
- 만약 호출하는 메소드가 Exception을 catch해 예외 상황을 해결하거나 문제를 해결할 수 없다면 Unchecked Exception을 사용하라.
- 명확하지 않다면 Unchecked Exception을 사용하라.
기존에는 예외 클래스는 주로 Exception을 상속 받아서 Checked Exception으로 작성하는 경우가 많았지만, 요즘은 예외처리를 선택적으로 할 수 있도록 RuntimeException을 상속 받아서 작성하는 쪽으로 바뀌어가고 있다. Checked Exception는 반드시 예외처리를 해주어야 하기 때문에 예외처리가 불필요한 경우에도 try-catch문을 넣어서 코드가 복잡해지기 때문이다.
프로그래밍 환경이 달라진만큼 필수적으로 처리해야만 할 것 같았던 예외들이 선택적으로 처리해도 되는 상황으로 바뀌는 경우가 종종 발생하고 있다.
예외처리하기
발생한 예외를 처리하지 못하면, 프로그램은 비정상적으로 종료되며 처리되지 못한 예외(Uncaught excpeption)는 JVM의 예외처리기(UncaughtExceptionHandler
)가 받아서 예외의 원인을 화면에 출력한다.
try-catch
try 블럭에서 예외가 발생하면 예외의 종류와 일치하는 단 한 개의 catch블럭만 수행한다.
try{
// 예외 발생할 가능성이 있는 문장
} catch (Exception1 | Exception11 e1){
// Exception1 또는 Exception11이 발생했을 때, 처리를 위한 로직
} catch (Exception2 e2){
// Exception2이 발생했을 때, 처리를 위한 로직
} catch (Exception3 e3){
// Exception3이 발생했을 때, 처리를 위한 로직
} catch (Exception4 e4){
// Exception4이 발생했을 때, 처리를 위한 로직
}
- try블럭 내에서 예외가 발생한 경우
- 발생한 예외와 일치하는 catch 블럭이 있는지 확인한다.
- 일치하는 catch 블럭을 찾게되면, 그 catch 블럭 내의 문장들을 수행하고 전체 try-catch 문을 빠져나와 그 다음 문장을 계속해서 수행한다. 만일 일치하는 catch 블럭을 찾지 못하면, 예외는 처리되지 못한다.
- try 블럭 내에서 예외가 발생하지 않은 경우
- catch 블럭을 거치지 않고 전체 try-catch 문을 빠져나가서 수행을 계속한다.
finally
finally 블럭은 예외의 발생 여부와 상관없이 실행되어야 할 코드를 포함시킬 목적으로 사용된다.
try{
// 예외 발생할 가능성이 있는 문장
} catch (Exception1 | Exception11 e1){
// Exception1 또는 Exception11이 발생했을 때, 처리를 위한 로직
} finally {
// 예외의 발생여부와 상관없이 항상 수행되어야 하는 문장
}
예외가 발생할 경우 try-catch-finally 순으로 실행되고, 예외가 발생하지 않을 경우 try-finally 순으로 실행된다.
finally with return
try{
// 예외 발생할 가능성이 있는 문장
} catch (Exception1 | Exception11 e1){
e1.printStackTrace();
} finally {
// 예외의 발생여부와 상관없이 항상 수행되어야 하는 문장
}
try 블럭에서 return문이 실행되는 경우에도 finally 블럭의 문장들이 먼저 실행된 후에 현재 실행 중인 메서드를 종료한다.
throw
- 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든다.
- 키워드 throw를 이용해서 예외를 발생시킨다.
throw new Exception("예외 발생!");
Checked vs Unchecked
- Checked Exception의 경우
public static void main(String[] args){
throw new Exception(); // Error!
}
Exception()의 경우 Checked Exception에 해당되고, Checked Exception의 경우는 예외처리가 되지 않으면 컴파일되지 않는다.
- Unchecked Exception의 경우
public static void main(String[] args){
throw new RuntimeException(); // Success!
}
위 코드는 예외처리를 해주지 않았음에도 불구하고 이전의 예제와는 달리 성공적으로 컴파일된다. 그러나 실행하면, RuntimeException()이 발생하여 비정상적으로 종료된다. RuntimeException()와 그 자손들은 Unchecked Exception에 해당되고, 프로그래머의 실수로 발생하는 것들이기에 예외처리를 강제하지 않는다.
메서드의 예외 선언 - throws
throws는 throw 값을 받는 곳에서 예외를 처리하도록 하여 예외처리의 주체를 바꿔준다.
void method() throws Exception1, Exception2 {
}
메서드의 선언부에 예외를 선언함으로써 메서드를 사용하려는 사람이 메서드의 선언부를 보았을 때, 이 메서드를 사용하기 위해서는 어떠한 예외들이 처리되어져야 하는지 쉽게 알 수 있다.
자동 자원 반환 - try-with-resources
입출력에 사용되는 클래스 중에서 사용한 후에 꼭 닫아줘야 하는 로직들이 있다. 이러한 로직들을 finally 문에 작성하여 해결할 수 있다.
try{
fis = new FileInputStream("score.dat");
dis = new DataInputStream(fis);
...
} catch(IOException ie){
ie.printStackTrace();
} finally {
dis.close();
}
하지만 finally 문 내에서 close()
가 예외를 발생시킬 수 있다. 따라서 코드를 다음과 같이 수정할 수 있다.
try{
fis = new FileInputStream("score.dat");
dis = new DataInputStream(fis);
...
} catch(IOException ie){
ie.printStackTrace();
} finally {
try{
if(dis != null) dis.close();
}catch(IOException ie){
ie.printStackTrace();
}
}
close()
가 문제가 발생하는 것의 예외처리를 했지만 코드가 너무 복잡해져 가독성에 좋지 않다. 이를 try-with-resources로 보완할 수 있다.
try ( FileInputStream fis = new FileInputStream("score.dat");
DataInputStream dis = new DataInputStream(fis);){
...
} catch(EOFException e){
System.out.println("완료!");
} catch(IOException ie){
ie.printStackTrace();
}
try-with-resources문의 괄호()안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()
를 호출하지 않아도 try블럭을 벗어나는 순간 자동적으로 close()
가 호출된다.
이처럼 try-with-resources문에 의해 객체의 close()
를 호출하기 위해서는, 클래스가 AutoCloseable이라는 인터페이스를 구현해야 한다.