이 글에서는 Java에서 사용하는 예외 처리 방법을 알아볼 것이다.
Error vs Exception
Java에서 Error는 치명적인 오류이며 프로그램 내에서 수습이 불가능하다. Stack overflow나 Out of memory같이 JVM의 실행에 문제가 생겼다는 것이므로 대처할 수 없다.
반면 Exception은 Error보다는 경미한 오류이며 프로그램 내에서 수습할 수 있다. Java의 exception은 runtime-error과 compile error로 나뉘며 runtime error는 unchecked exception, compile error는 checked exception으로 부른다. (이 문장에서의 error는 Java에서 사용하는 Error와는 다르다! runtime error는 실행 중에 발생하는 오류, compile error는 컴파일 중에 발생하는 오류 뜻이다.)
checked exception은 compile error인 만큼 꼭 예외처리를 해 주어야 하며 그렇지 않은 경우 compile error가 발생한다. 반면 unchecked exception은 runtime error인 만큼 예외처리를 하지 않아도 되지만 오류가 발생할 경우 프로그램이 바로 종료된다.
계층 구조는 아래와 같다.
Throwable class에는 getMessage method와 printStackTrace method(발생 경로 추적) 등의 method가 있다. 이를 활용해 어디서 오류가 났는지, 어떤 오류가 났는지 확인할 수 있다.
예외 처리 Exception Handling
프로그램 실행 중에 exception이 발생했을 때 정상적인 실행을 유지하기 위해 작성하는 것이 exception handling이다. Java에서는 try-catch-finally문으로 exception handling한다.
try-catch-finally
try-catch-finally block 구조는 다음과 같이 생겼다.
import java.io.IOException;
// App.java
public class App {
public static void main(String[] args) throws Exception {
try{
// exceptionable code
}
catch(ArithmeticException arithmeticException){
// handle exception 1
}
catch(ArrayIndexOutOfBoundsException | NullPointerException runtimeException){
// handle exception 2
// | 연산자를 사용해 여러개의 catch문을 한 번에 작성할 수 있다.
}
catch(Exception e){
// exception 1이 발생하지 않았다면 아래로 내려감
}
finally{
// always executed
}
}
}
작성한 것처럼 하나의 catch에 여러 개의 exception을 한 번에 넣어 코드의 중복을 막을 수도 있다.
catch문을 여러 개 작성할 수도 있다. 다만 catch문을 여러 개 작성하는 경우 catch문은 switch문과 동일하게 위에서부터 훑고 내려가며 하나의 exception이 발생하는 즉시 해당 catch block으로 이동한다. 또한 모든 exception은 hierarchy 관계에 있다. 상위 exception class가 위쪽의 catch에 위치하고 하위 exception class가 아래의 catch에 위치해 있다면 하위 exception class가 발생해도 polymorphism에 의해 위쪽의 catch문에 걸러지기 때문에 상위 exception이 아래쪽의 catch문에 위치해야 한다.
try-catch-finally의 실행 순서
- try문이 실행된다
- exception이 발생했으면 exception이 발생한 위치 다음에 catch문이 실행된다.
- exception이 발생하지 않았으면 try문 끝까지 실행된다.
- finally문이 실행된다
다만 try나 catch문에 return이나 throw문이 있다면 그 return문이나 throw문이 실행되기 직전까지 코드가 수행된 후 finally문이 실행된다. 그 이후에 return문이나 throw문이 실행된다. finally문은 항상 실행된다. 다만 예외적으로 System.exit()이 explicitly 호출되거나 JVM이 프로그램을 종료시키는 경우에는 실행되지 않는다.
몇 가지 예시를 보자.
// App.java
public class App {
public static String tryReturnExample(int i){
try{
System.out.println("try block executed");
int a = 1 / i;
return "try block return";
}
catch(Exception e){
System.out.println("catch block executed");
}
finally{
System.out.println("finally block executed");
}
return "exception handling end";
}
public static String catchReturnExample(int i){
try{
System.out.println("try block executed");
int a = 1 / i;
}
catch(Exception e){
System.out.println("catch block executed");
return "catch block return";
}
finally{
System.out.println("finally block executed");
}
return "exception handling end";
}
public static String finallyReturnExample(int i){
try{
System.out.println("try block executed");
int a = 1 / i;
}
catch(Exception e){
System.out.println("catch block executed");
}
finally{
System.out.println("finally block executed");
return "finally block return";
}
// return "exception handling end";
// compile error : unreachable code
}
public static void main(String[] args) throws Exception {
// try문에서 return
System.out.println("** try normal");
System.out.println(tryReturnExample(1)); // normal
System.out.println("");
System.out.println("** try exception");
System.out.println(tryReturnExample(0)); // exception
System.out.println("");
// catch문에서 return
System.out.println("** catch normal");
System.out.println(catchReturnExample(1)); // normal
System.out.println("");
System.out.println("** catch exception");
System.out.println(catchReturnExample(0)); // exception
System.out.println("");
// finally문에서 return
System.out.println("** finally normal");
System.out.println(finallyReturnExample(1)); // normal
System.out.println("");
System.out.println("** finally exception");
System.out.println(finallyReturnExample(0)); // exception
System.out.println("");
}
}
** try normal
try block executed
finally block executed
try block return
** try exception
try block executed
catch block executed
finally block executed
exception handling end
** catch normal
try block executed
finally block executed
exception handling end
** catch exception
try block executed
catch block executed
finally block executed
catch block return
** finally normal
try block executed
finally block executed
finally block return
** finally exception
try block executed
catch block executed
finally block executed
finally block return
try문에서 exception이 발생하지 않은 경우에는 try - finally - 이후에 있는 로직이 실행된다.
try문에서 exception이 발생한 경우에는 try - catch - finally 순서대로 실행된다.
만약 중간에 return이 있는 경우 return이 실행되는 것을 볼 수 있다.
그럼 이렇게 해보자.
// App.java
public class App {
public static String wiredExample(int i){
try{
System.out.println("try block executed");
int a = 1 / i;
return "try block return";
}
catch(Exception e){
System.out.println("catch block executed");
return "catch block return";
}
finally{
System.out.println("finally block executed");
return "finally block return";
}
// return "exception handling end";
// compile error : unreachable code
}
public static void main(String[] args) throws Exception {
System.out.println("** normal");
System.out.println(wiredExample(1)); // normal
System.out.println("");
System.out.println("** exception");
System.out.println(wiredExample(0)); // exception
System.out.println("");
}
}
자. 어떤 값이 출력될까? 이렇게 코드를 짜면 exception이 발생하지 않으면 try문에서 return하고, exception이 발생하면 catch문에서 return하는 것을 예상하고 이렇게 코드를 짰을 것이다.
** normal
try block executed
finally block executed
finally block return
** exception
try block executed
catch block executed
finally block executed
finally block return
그러나 실행 결과는 위와 같이 모두 finally block에서 return되었다.
위에서 설명했듯 try나 catch문에 return이나 throw문이 있다면 그 return문이나 throw문이 실행되기 직전까지 코드가 수행된 후 finally문이 실행된다. 그 이후에 return문이나 throw문이 실행된다. 그러나 finally block에서 return을 해버리면 [try나 catch문의 리턴문 직전까지 수행] - [finally block 진입] - [finally block의 return문 실행]이 되기 때문에 이러한 일이 발생한다.
따라서 finally block에서 throw나 return을 하는 것을 지양해야 한다!
try-with-resource
java 7에서 추가된 기능이다. file이나 DB, network 등의 resource를 사용하는 경우 꼭 finally에서 닫아 주어야 한다. 그러나 finally문에서 resource 할당 해제를 잊을 수도 있고 개발자가 코드 해제에 대한 부담을 지기 때문에 실수에 대한 리스크가 크다. 또한 finally문에서 resource를 close할 때도 resouce가 null인 경우가 있을 수 있기 때문에 try-catch-finally를 사용하고, finally문 안에서 또 try-catch문을 사용해야 했다. 벌써 어지럽다.
이것을 고친 것이 try-with-resource이다.
try-with-resource문은 try문 이후에 괄호를 추가하고 그 괄호에 해제하고 싶은 resource를 넣는다. 여러 개를 넣고 싶다면 ;를 이용해 여러 문장을 작성하면 된다. try block이 끝나자마자 ()에서 할당한 resource를 해제한다.
// App.java
public class App {
public static void main(String[] args) throws Exception {
try(FileInputStream fis = new FileInputStream("file.txt");
FileInputStream fis2 = new FileInputStream("file.txt"); ){
fis.read();
throw new Exception();
}
catch(Exception e){
// ...
}
}
}
AutoCloseable
try-with-resource문에 사용하는 resource object는 AutoCloseable interface를 implement해야 한다.
예외처리 방법
지금까지 try-catch-finally문에 대해 알아보았다. 이제는 이걸 어떻게 쓸지 볼 것이다. 크게 복구, 회피, 전환 3가지가 있다.
예외 복구
n번 재시도해 해당 exception을 복구하도록 한다. 아래와 같은 방식으로 while문을 쓴다.
private void recoverException() {
int maxTryNumber = 5;
while(maxTryNumber > 0) {
try {
// exceptionable
} catch(Exception e) {
// exception occur
} finally {
// clear resource
}
}
// 최대 횟수 실패시 예외 Throw
throw new RecoverFailException();
}
예외 회피 & 전환
해당 method를 call한 곳에서 처리하도록 throw Exception을 사용하는 방식이다.
throws keyword가 있는 method는 반드시 try block 내에서 호출되어야 한다. 따라서 이 경우는 주의해서 사용해야 하는데, method가 throw를 하면 할수록 더 상위의 method에서 처리해야 하기 때문이다. 이는 transaction과도 연관되어 있기 때문에 해당 method에서 exception handling을 하지 않고 상위의 method에서 exception handling하는 것이 훨씬 좋을 때만 사용한다.
이 때, 단순히 발생한 exception을 throw하면 회피이고, exception이 발생함을 알고 특정한 exception을 명시해서 throw한다면 전환이다.
private void recoverException() throws Exception {
try {
// exceptionable
} catch(Exception e) {
// exception occur
throw e; // 회피
throw new Exception(); // 전환
} finally {
// clear resource
}
}
Code Transaction
다음 예시를 보자.
// App.java
public class App {
private static void myMethod1(){
try{
// ...
}
catch(Exception e){
// ...
}
finally{
// ...
}
}
private static void myMethod2(){
try{
// ...
}
catch(Exception e){
// ...
}
finally{
// ...
}
}
public static void main(String[] args) throws Exception {
myMethod1();
myMethod2();
}
}
myMethod1에서 exception이 발생해도 myMethod1 내부에서 exception handling하기 때문에 myMethod2는 실행된다. 그렇다면 아래의 경우는 어떨까?
// App.java
public class App {
private static void myMethod1() throws Exception {
try{
// ...
}
catch(Exception e){
// ...
throw e;
}
finally{
// ...
}
}
private static void myMethod2() throws Exception {
try{
// ...
}
catch(Exception e){
// ...
throw e;
}
finally{
// ...
}
}
public static void main(String[] args) throws Exception {
try {
myMethod1();
myMethod2();
}
catch (Exception e) {
// ...
}
finally{
// ...
}
}
}
myMethod1에서 exception이 발생하면 해당 method를 call한 곳에서 처리하게 throw하고 있다. 그러면 myMethod1 뒤에 있는 myMethod2는 실행되지 않는다.
이렇듯 예외처리를 어떻게 하느냐에 따라 다음 코드가 실행될 수도 있고, 아닐 수도 있다. 따라서 좋은 방향을 선택해야 할 것이다.
Custom Exception
위에서 사용했던 그림이다. Java에서 error, exception, runtime exception은 hierarhcy를 가지고 있다. 즉 polymorphism이 성립한다는 말이다. 그래서 runtime exception이 발생해도 catch(Exception e)로 잡을 수 있는 것이다.
hierarchy를 가진다는 말은, 다르게 말하자면 extends할 수 있다는 말이다. 내가 원하는 error나 exception이 있을 때 이를 extend해 사용할 수 있다.
'Development > Java' 카테고리의 다른 글
[Java] Generic (0) | 2023.03.02 |
---|---|
[Java] Java Virtual Machine (0) | 2023.03.01 |
[Java] abstract class vs interface (0) | 2023.02.27 |
[Java] Polymorphism - static polymorphism, dynamic polymorphism, casting (0) | 2023.02.23 |
[Java] Inheritance - access modifier, super, overriding (0) | 2023.02.23 |