-
예외처리와 Try With ResourcesJava 2024. 5. 30. 16:04
응용프로그램에서 문제가 발생하면 보통 에러 혹은 예외를 던진다.
자바의 에러 구조는 다음과 같다.
가장 상위 인터페이스로 Throwable 이 있고
여기서 Throwable을 상속받는 두가지로 예외는 Exception, 에러는 Error로 나뉜다.
그리고 Exception을 상속받는 클래스 중 RuntimeException은 런타임 도중 발생하는 예외를 잡는다.
예외는 던지거나 잡을 수 있다.
던지고싶다면 메소드 선언부에서 throws 키워드로 던지고싶은 Exception을 나열하면된다.
잡으려면 try-catch~finally가 있다.
try ~ catch ~ finally에서 중요한 점은
Exception이 catch 되더라도 finally는 발동한다.
public int test(){ try{ Class.forName("java.lang.String"); return 12; }catch(ClassNotFoundException e){ e.printStackTrace(); }finally{ // finally 구문은 Exception이 발생해도 반드시 실행한다. return 10; } }
Try with Resources
https://mangkyu.tistory.com/217
다음으로는 try with resources 이다.
이는 주로 FileInputStream같은 열어주면? 닫아줘야하는 객체가 있을 때,
Auto로 finally없이 작업이 끝나면 닫아주는 역할을 수행한다.
사용법
try with resources 를 사용하려면 AutoCloseable 인터페이스의 close() 메서드를 구현해야 한다.
아마 try절에 소괄호 속에 들어가는 객체에 close메서드를 호출해서 그런게 아닐까 싶다.
참고) 자바 9에서 부터는 아래와 같이 객체 생성부와 try속 객체를 넣는곳을 분리할 수 있다.
참고2) 아래와 같이 사용하려면 AutoCloseable을 구현한 객체가 final이거나 effectively final이어야 한다.
void test(){ MyResource myRes1 = new MyResource(); MyResource myRes2 = new MyResource(); try(myRes1;myRes2){ // Java 9부터 가능 // 실행되어야 하는 구문 }catch(Exception e){ // catch절 구문 } }
이러한 try with resources를 사용하는데 있어서 크게 두가지 이유를 위 블로그에서 참조할 수 있었다.
스택 트레이스 누락
원래라면 다음과 같은 코드가 일반적일 것 이다.
class MyResource implements AutoCloseable{ // AutoCloseable을 구현해서 close 함수를 구현한다. public void open(){ System.out.println("열어요"); throw new IllegalArgumentException(); } @Override public void close(){ System.out.println("닫아요"); throw new IllegalArgumentException(); } } public class Main { MyResource myRes1; MyResource myRes2; public void temp(){ try{ myRes1 = new MyResource(); myRes1.open(); }finally{ if(myRes1 != null){ myRes1.close(); } } } public static void main(String[] args){ Main main = new Main(); main.temp(); } }
이렇게 작성하고 실행하면 open에 발생하는 Exception이 스택 트레이스에 잡히지 않는다.
그런데 아래와 같이 바꾸면 결과에 두가지 Exception이 모두 잡히는것 처럼 보인다.
class MyResource implements AutoCloseable{ public void open(){ System.out.println("열림"); throw new IllegalArgumentException(); } @Override public void close() { System.out.println("닫힘"); throw new IllegalArgumentException(); } } public class Main { MyResource myRes1; MyResource myRes2; public void test(){ myRes1 = new MyResource(); try{ myRes1.open(); } catch(Exception e){ e.printStackTrace(); } finally { if(myRes1 != null) myRes1.close(); } } public static void main(String[] args) { Main main = new Main(); main.test(); } }
열림 닫힘 java.lang.IllegalArgumentException at MyResource.open(Main.java:8) at Main.test(Main.java:25) at Main.main(Main.java:37) Exception in thread "main" java.lang.IllegalArgumentException at MyResource.close(Main.java:13) at Main.test(Main.java:30) at Main.main(Main.java:37)
두가지 Exception이 모두 잡힌것 처럼 보이지만
main 메서드를 실행하는 쓰레드에서 Exception이 발생하였다고 나온다.
즉, finally에서 던져진 예외가 catch에서 처리되지 못하고, JVM에 던지게 된 것이다.
이걸 막기위해서 try with resources를 사용한다면
public void temp(){ MyResource myRes1 = new MyResource(); try(myRes1){ myRes1.open(); }catch(Exception e){ e.printStackTrace(); } }
open과 close에 발생하는 모든 Exception이 catch절에 의해 잡히게 된다.
열어요 닫아요 java.lang.IllegalArgumentException at MyResource.open(Main.java:11) at Main.temp(Main.java:27) at Main.main(Main.java:34) Suppressed: java.lang.IllegalArgumentException at MyResource.close(Main.java:17) at Main.temp(Main.java:26) ... 1 more
자원 반납 누락
이제 위의 코드에서 open에 있는 throw를 없애고, 두개의 MyResource객체를 만들어서 finally구문에서 close하자.
class MyResource implements AutoCloseable{ public void open(){ System.out.println("열림"); } @Override public void close() { System.out.println("닫힘"); throw new IllegalArgumentException(); } } public class Main { MyResource myRes1; MyResource myRes2; public void test(){ myRes1 = new MyResource(); myRes2 = new MyResource(); try{ myRes1.open(); myRes2.open(); } catch(Exception e){ e.printStackTrace(); } finally { if(myRes1 != null) myRes1.close(); if(myRes2 != null) myRes1.close(); } } public static void main(String[] args) { Main main = new Main(); main.test(); } }
스택 트레이스는 다음과 같이 나온다.
열림 열림 닫힘 Exception in thread "main" java.lang.IllegalArgumentException at MyResource.close(Main.java:12) at Main.test(Main.java:29) at Main.main(Main.java:36)
finally 에 있는 myRes1의 close에서 발생한 Exception이 처리되지 못한채로 JVM까지 처리 위임이 되었기에
비정상종료되어 myRes2의 닫힘이 발생할 수 없게 되었다.
이제 try with resources로 변경해보자.
public void test(){ MyResource myRes1 = new MyResource(); MyResource myRes2 = new MyResource(); try(myRes1;myRes2){ myRes1.open(); myRes2.open(); }catch(Exception e){ e.printStackTrace(); } }
스택 트레이스는 다음과 같이 달라진다.
열림 열림 닫힘 닫힘 java.lang.IllegalArgumentException at MyResource.close(Main.java:12) at Main.test(Main.java:23) at Main.main(Main.java:30) Suppressed: java.lang.IllegalArgumentException at MyResource.close(Main.java:12) at Main.test(Main.java:20) ... 1 more
이제 myRes1이 Exception을 던지더라도 myRes2까지 실행하는데 영향을 주지않았고
각 close() 메서드 호출동안에 발생하는 Exception또한 catch절에 잘 잡히게 되었다.
그 원리는 bytecode를 까보면 알 수 있다.
내부적으로 작성자가 작성한 catch절을 제외하고도 exception이 발생할 부분들에 대하여 컴파일러가 try-catch로 감싸놓고
Throwable에 있는 addSuppressed를 통해 누락되지 않도록 안쪽부터 발생한 Exception을 전부 누적해놓는다.
public void temp3() { // try with resources 사용 (JAVA 9에서는 아래와 같이 AutoCloseable을 구현한 객체를 바로 넣을 수 있음) // suppressed를 통해 누락없이 예외를 잡을 수 있음 // myRes1 = new MyResource(); 이렇게 작성하면 effectively final이어야 한다는 에러발생 MyResource myRes1 = new MyResource(); try(myRes1) { myRes1.open(); } catch (Exception e) { e.printStackTrace(); } } // 위의 코드를 bytecode로 보면 (인텔리제이 기준이다.)' public void temp3() { MyResource myRes1 = new MyResource(); // 순서상으로 누적되어야 하는건 open() 메소드 이후 close()메소드 이기 때문에 내부적으로 감싸준듯 하다. try { MyResource var2 = myRes1; try { myRes1.open(); } catch (Throwable var6) { if (myRes1 != null) { try { var2.close(); } catch (Throwable var5) { var6.addSuppressed(var5); } } throw var6; } if (myRes1 != null) { myRes1.close(); } } catch (Exception var7) { var7.printStackTrace(); } }
'Java' 카테고리의 다른 글
함수형 인터페이스 (1) 2024.04.18 가비지 컬렉션 - 가비지 컬렉션의 개념와 작동 방식 (1) 2024.04.13