본문 바로가기

Java/생활코딩

자바(Java) 개념 정리 - 예외처리 #19

숙명 vs 운명

 

ERROR vs EXCEPTION

 

ERROR : 우리가 만든 프로그램의 문제가 아닌, 프로그램이 동작하는 환경의 문제.

 

EXCEPTION : 내가 짠 코드가 예상했던 상황과 다른 상황에 직면했을 때.

 

public class ExceptionApp {
       public static void main(String[] args) {
       System.out.println(1);
       System.out.println(2/0);
       System.out.println(3);
       }
}

이 코드를 입력했을 때, 다음과 같은 에러가 발생한다.

1
Exception in thread "main" java.lang.ArithmeticException: / by zero
       at ExceptionApp.main(ExceptionApp.java:6)

이렇게 예외가 발생해서 에러가 나는 경우를 의도했으면 성공이다. 하지만 그럴리는 없을 것이다.

 

의도가 된 것이 아니라면 프로그래머의 실수이다. 이 예외를 처리하는 방법이 예외처리이다.

그러면 어떻게 예외를 처리할까?

예외가 날 것 같은 코드를 try로 격리를 시켜주자.

       try {
              System.out.println(2/0);
       } catch(ArithmeticException e) {
              System.out.println("잘못된 계산이네요.");
       }

 

참고로 e는 변수이므로 다른 단어가 와도 된다. 편의상 보통 e를 사용한다.

 

위 코드는 System.out.println(2/0)이라는 것을 시도(try)하는 것이다. 하지만 이 경우에서 위에 발생한 ArithmeticException이 발생하면 처리해야 하는 작업을 catch로 잡아주는 것이다. 만일 try-catch를 사용하지 않으면 거기서 실행이 멈춘다. 하지만 이렇게 잡아주면 실행이 계속 된다.

public class ExceptionApp {
       public static void main(String[] args) {
       System.out.println(1);
       int[] scores = {10,20,30};
       try {
              System.out.println(scores[3]);
       } catch(ArrayIndexOutOfBoundsException e) {
              System.out.println("배열 범위 초과 됐어요 ㅋ");
       }
       
       try {
              System.out.println(2/0);
       } catch(ArithmeticException e) {
              System.out.println("잘못된 계산이네요.");
       }
       System.out.println(3);
       
       }
}

같은 방법으로 배열의 예외를 잡아준 모습이다. 그러면 try 괄호 안에서는 코드 작업이 어떻게 진행될까?

 

public class ExceptionApp {
       public static void main(String[] args) {
       System.out.println(1);
       int[] scores = {10,20,30};
       try {
              System.out.println(2);
              System.out.println(scores[3]);
              System.out.println(3);
              System.out.println(2/0);
              System.out.println(4);
       } catch(ArithmeticException e) {
              System.out.println("잘못된 계산이네요.");
       } catch(ArrayIndexOutOfBoundsException e) {
              System.out.println("배열 범위 초과 됐어요 ㅋ");
       }
       System.out.println(5);
       
       }
}

// 출력
1
2
배열 범위 초과 됐어요 ㅋ
5

try {} 안에서 예외처리가 실행되면 그 아래의 코드는 실행될 기회를 잃는다.

 

만약에 하나의 catch문으로 try 배열을 처리하고 싶을 때는 어떻게 해야할까?

일단 ArithmeticException 클래스의 내용을 보자.

 

https://docs.oracle.com/javase/7/docs/api/

 

위의 상속관계를 통해서 조상 클래스를 찾아보자. 

그리고 catch문에 조상 클래스를 넣어주자.

 

이후의 catch문이 동시에 에러가 난다. 그 이유는 Exception이 ArithmeticException과 ArrayIndexOutOfBoundsException의 공통된 조상 클래스이기 때문이다. 

 

예외는 상속을 통해서 부모 자식의 관계가 있다. 부모의 자식의 해당하는 어떤 상황이 발생하던 예외처리를 해준다. 조상 클래스 이후에 자식 클래스로 catch문을 쓸 수는 없지만, 자식 클래스 이후에 조상클래스를 사용할 수 있다. 그리고 catch문은 코드 순차적으로 실행된다.


e의 비밀

 

지금까지 예외처리에 변수 e를 사용해왔다. 이 변수는 어떻게 쓰이는 것일까?

detailMessage : Index 3 out of bounds for length 3 / by zero라고 나온다. 이 메시지를 출력값에 같이 보고 싶을 때,

 

System.out.println("계산이 잘못된 것 같아요." + e.getMessage());

       

이렇게 코드를 변경해주면 된다.

 

public class ExceptionApp {
       public static void main(String[] args) {
       System.out.println(1);
       int[] scores = {10,20,30};
       try {
              System.out.println(2);
//            System.out.println(scores[3]);
              System.out.println(3);
              System.out.println(2/0);
              System.out.println(4);
       } catch(ArithmeticException e) {
              System.out.println("계산이 잘못된 것 같아요." + e.getMessage());
       }
       
       catch(Exception e) {
              System.out.println("뭔가 이상합니다. 오류가 발생했습니다.");
       }
       
       System.out.println(5);
       
       }
}

//출력
1
2
3
계산이 잘못된 것 같아요./ by zero
5

그런데 예외처리를 하더라도, 어떤 예외가 나왔는지 보고를 받고 싶으면 어떻게 해야 할까? try catch문을 삭제하면 이전과 같이 빨간 글씨로 예외 상황을 보고 받을 수 있다. 하지만 그러면 해당 에러가 발생한 지점에서 실행이 멈추기 떄문에 그 이후의 출력값을 확인할 수 없다. 이 때

public class ExceptionApp {
       public static void main(String[] args) {
       System.out.println(1);
       int[] scores = {10,20,30};
       try {
              System.out.println(2);
//            System.out.println(scores[3]);
              System.out.println(3);
              System.out.println(2/0);
              System.out.println(4);
       } catch(ArithmeticException e) {
              System.out.println("계산이 잘못된 것 같아요." + e.getMessage());
              e.printStackTrace();
       }
       
       catch(Exception e) {
              System.out.println("뭔가 이상합니다. 오류가 발생했습니다.");
       }
       
       System.out.println(5);
       
       }
}

이렇게 e.printStackTrace()를 작성하면

위와 같이 나타날 수 있다.


check는 컴파일러가 한다. Checked Exception은 컴파일 조차 되지 않는 Exception이고 UnChecked Exception은 try-catch문으로 개발자가 직접 잡아줘야 하는 Exception이다. 

 

개념에 대한 궁금증이 있을 때, google image 검색을 이용하자. 

이렇게 예외의 상속관계를 볼 수 있다. 여기서 Uncheked Exception은 RuntimeException을 포함한 그 자식들이다. Checked Exception은 RuntmeException을 제외한 Exception의 자식들이다. 대표적인 예외가 IOException이다.

 

IOException에서 I는 Input, 컴퓨터에 데이터를 파일에 저장한다. O는 Output, 컴퓨터 파일에서 데이터를 불러온다. IOException은 인풋 아웃풋 하는 과정에서 쉽게 발생하는 예외적 상황을 위함이다. 인풋 아웃풋은 외부와 통신을 하기 떄문에 예외적 상황이 잦다. 파일을 읽으려고 했는데 파읽이 없는 것이다. 

import java.io.FileWriter;
import java.io.IOException;
public class CheckedExceptionApp {
       public static void main(String[] args) {
              FileWriter f = new FileWriter("data.txt");
              f.write("Hello");
              f.close();
       }
}

IO작업은 중요한게, 여러 파일을 동시에 읽긴 가능해도 쓰진 못하게 해야한다. 그래서 close 메소드를 써야한다. 그런데 보면

이와 같은 에러가 발생한다. 예외처리를 해줘야 한다는 메시지다. IOException은 컴파일러가 check를 하는 예외이기 때문에 반드시 예외처리를 해줘야 컴파일을 할 수 있다.

import java.io.FileWriter;
import java.io.IOException;
public class CheckedExceptionApp {
       public static void main(String[] args) {
              try {
              FileWriter f = new FileWriter("data.txt");
              f.write("Hello");
              f.close();
              } catch (IOException e) {
                     e.printStackTrace();
              }
       }
}

checked Exception은 try-catch나 throws를 통해서 조치를 해줘야 한다. 이러한 처리를 해주지 않으면 실행을 용인하지 않는다.


import java.io.FileWriter;
import java.io.IOException;
public class CheckedExceptionApp {
       public static void main(String[] args) {
              try {
                     FileWriter f = new FileWriter("data.txt");
                     f.write("Hello");
                     // close를 하기 전에 예외가 발생할 수 있기 때문에 finally로  처리해야 합니다.
              } catch (IOException e) {
                     e.printStackTrace();
              } finally {
                     f.close();
              }
       }
}

finally를 사용하면 try에서 예외가 발생했건 안했건 괄호 내용이 실행된다. 

 

f를 try가 아닌 바깥에서 타입을 정해주고 초기화를 해줘야 한다. 그 이유는 try안에 있는 f 선언 코드가 예외상황일 수 있기 때문이다. 그렇게 되면 f는 초기화가 되지도 않고 데이터 타입도 정해지지 않았으므로 f가 존재하지 않기 때문이다. 하지만 finally는 예외처리 이후에도 실행이 되므로 문제가 발생한다.

 

그래서 try문 이전에 f의 초기화선언을 해주고 조건식을 넣어준다. 또한, 이 코드도 FileWriter를 사용하기 때문에 try-catch를 사용해준다.

import java.io.FileWriter;
import java.io.IOException;
public class CheckedExceptionApp {
       public static void main(String[] args) {
              FileWriter f = null;
              try {
                     f = new FileWriter("data.txt");
                     f.write("Hello");
                     // close를 하기 전에 예외가 발생할 수 있기 때문에 finally로  처리해야 합니다.
              } catch (IOException e) {
                     e.printStackTrace();
              } finally {
                     // 만약에 f가 null이 아니라면
                     if(f != null) {
                           try {
                                  f.close();
                           } catch(IOException e) {
                                  e.printStackTrace();
                           }
                     }
                     
              }
       }
}

 

자 이렇게 까지 하려니 코드가 너~~무 복잡해진다. 자바 개발자도 이 복잡한거를 안다. 그래서 이를 해결해주려고 추가적으로 기능을 만들었다.


기존에 있는 코드를 정리하자.

import java.io.FileWriter;
import java.io.IOException;
public class CheckedExceptionApp {
       public static void main(String[] args) {
              FileWriter f = null;
              f = new FileWriter("data.txt");
              f.write("Hello");
              f.close();
       
       }
}

FileWriter는 실행을 마치면 무조건 close를 해줘야 한다. 이를 편하게 해주기 위해 AutoCloseable이 있다.

 

https://docs.oracle.com/javase/7/docs/api/java/io/FileWriter.html

 

FileWirter 클래스는 AutoCloseable 인터페이스를 구현하고 있다. AutoCloseable를 구현하고 있는 클래스는 모두 try-with-resource를 가지고 있다.

 

try("close를 해야하는 클래스를 인스턴스화 시키는 코드를 삽입") 

import java.io.FileWriter;
import java.io.IOException;
public class CheckedExceptionApp {
       public static void main(String[] args) {
              // try with resource statements
              try(FileWriter f = new FileWriter("data.txt")) {
                     f.write("Hello");
                     f.close();
              } catch(IOException e) {
                     e.printStackTrace();
              }
       
       }
}

자바는 이 모든 작업이 끝나고 나서 자동으로 f.close를 내부적으로 수행을 해준다. 이 것을 try with resource statements 이다.


이렇게 나의 예외를 만들 수도 있다.

import java.io.FileWriter;
import java.io.IOException;
public class CheckedExceptionApp {
       public static void main(String[] args) {
              throw new RuntimeException("무언가 문제가 있습니다.")
       
       }
}

//출력
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
       Syntax error, insert ";" to complete BlockStatements
       at CheckedExceptionApp.main(CheckedExceptionApp.java:7)

 

예외는 다음과 같이 에러의 문제를 다음 예외처리 클래스로 넘긴(throws)다. 그래서 결국 try-catch를 해야 예외가 처리가 되고 그렇지 않으면 에러가 발생하는 것이다.

 

만일, 다음과 같은 코드가 있다고 하자.

import java.io.FileWriter;
import java.io.IOException;
public class CheckedExceptionApp {
       public static void main(String[] args) {
              FileWriter f = new FileWriter("./data.txt");
       
       }
}

//출력
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
       Unhandled exception type IOException
       at CheckedExceptionApp.main(CheckedExceptionApp.java:7)

 

이를 try - catch로 예외를 처리하면

import java.io.FileWriter;
import java.io.IOException;
public class CheckedExceptionApp {
       public static void main(String[] args) {
              try {
                     FileWriter f = new FileWriter("./data.txt");
              } catch (IOException e) {
                     e.printStackTrace();
              }
       
       }
}

이렇게 완성이 된다.

 

또, 이 예외를 다른데로 던져버릴 수 있다. 내가 예외처리 안할 테니깐 이 변수를 쓰는 쪽이하라며.

import java.io.FileWriter;
import java.io.IOException;
public class CheckedExceptionApp {
       public static void main(String[] args) throws IOException {
              FileWriter f = new FileWriter("./data.txt");
       
       }
}

던져버리고 싶은 예외 이름을 적으면 된다. 즉, 예외가 발생하게 되면 코드에서 예외를 처리하는 게 아니라 예외를 사용하는 곳에서 쓰고, 그 사용하는 곳이 throw를 하게 되면 그것을 또 사용하는 곳에서 예외를 처리해야한다.

 

내가 처리하는 것. try-catch

내가 처리하지 않고 남에게 미루는 것. throw

 

예외처리를 해주는 것은 유비무환의 마음가짐을 갖는 것.