siino's 개발톡

[Java] 예외 클래스와 try-catch-finally / try-with-resource 본문

Java

[Java] 예외 클래스와 try-catch-finally / try-with-resource

siino 2024. 1. 11. 16:35

예외의 두가지 종류

Checked Exception - 예외가 발생할 가능성이 높기 때문에 컴파일 시 예외처리가 없다면 컴파일 오류 발생

Un-Checked Exception - 컴파일 과정에서 예외 처리 코드가 있는지 검사하지 않음.

 

java.lang.Exception 하위에는,

 - 일반예외들...

 - RuntimeException 이 있다.

즉, 모든 예외 클래스는 Exception 클래스를 상속받는다.

 

일반 예외(체크)의 종류 - ClassNotFoundException, InterruptedException, 

런타임 예외(언체크)의 종류 - NullPointerException, ClassCastException, NumberFormatException

 

try - catch - finally

1. try블록에는 예외 발생 가능 코드가 위치

2. try 블록의 예외 발생x 

3. finally블록은 생략가능.

예외 발생 여부와 상관없이 항상 실행할 내용이 있을 경우에만 finally블록을 작성.

심지어 try 블록과 catch 블록에서 return 문을 사용하더라도 finally 블록은 항상 실행된다.

 

## try - catch - finally 기본 예시

public class TryCatch {

    public static void main(String[] args) {
        String str = "a123";
        int i = stringToInt(str);
        System.out.println(i);
    }

    static int stringToInt(String str){
        try {
            int ret = Integer.parseInt(str);
            return ret;
        } catch (NumberFormatException e) {
            System.out.println("숫자가 아닌 입력입니다.");
            return Integer.MIN_VALUE;
        } finally {
            System.out.println("무조건 실행된다.");
        }
    }
}

출력결과


주의해야할 점은 finally블록에 종료조건(return)문이 있다면 try, catch블록이 return하거나 throw하는 것은 무시된다.

따라서 finally 블럭에서 return 하는 것은 의도치 않은 결과를 얻을 수 있으니 하지 않는 것을 권장한다.

intellij에서도 'finally' block can not complete normally, 'return' inside 'finally' block 이라며 경고 메시지가 뜬다.

 

## 주의해야 할 두 가지 예시

# 1 - finally 블록에서 기본 타입 변경

public static void main(String[] args) {
        int ans = test();
        System.out.println(ans);
    }

    static int test(){
        int n = 5;
        try {
            System.out.println("---------try 블록--------");
            return n;
        } finally {
            System.out.println("---------finally 블룩 ---------");
            n = 10;
        }
    }

여기서 중요한 것은 return 문이 실행될 때 반환값이 결정되고, 그 이후 finally 블록의 실행이 이 반환값에 영향을 미치지 않는다는 점입니다.

 

# 2 - finally 블럭에서 참조 타입 변경

 public static void main(String[] args) {
        StringBuilder ans = test();
        System.out.println(ans);
    }

    static StringBuilder test(){
        StringBuilder str = new StringBuilder("ABC");
        try {
            System.out.println("---------try 블록--------");
            return str;
        } finally {
            System.out.println("---------finally 블룩 ---------");
            str.append("DEF");
        }
    }

 

 

StringBuilder 객체는 참조 타입이므로 return 문이 실행될 때 실제 객체가 아니라 객체의 참조(주소)가 반환됩니다. try 블록에서 str의 참조를 반환하고, finally 블록에서 이 참조가 가리키는 객체 (str)에 대한 변경이 일어납니다. 이 경우 finally 블록에서 str.append("DEF")를 통해 객체의 내용이 변경됩니다.

 


try - catch - finally 문제점

그럼 이 try-finally 블럭을 보통 어디에 많이 사용하느냐..

바로 자원을 회수할 때 주로 많이 사용했다.

우리가 Web 프로젝트를 진행할 때 자주 사용하는 DB Connection, Driver 등과 같은 클래스를 사용할 때,

또는 파일을 읽기 위해 Reader를 사용할 때 등..

 

Reader를 사용한 예제를 알아보자.

텍스트 파일을 불러오고 읽고 싶을 때 try-finally를 사용하여 BufferedReader 자원을 해제해주었다.

 

public static void main(String[] args) {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader("./Algorithm/day45/test.txt"));
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

 

이렇게 위와 같이 try-finally 블럭을 사용할 수 있는데, 여기에서 왜 finally 블럭안에 또 try-catch 구문이 있을까?

바로 finally 블럭에서 예외가 발생하면 try 블럭에서 발생한 예외는 철저히 "무시되기" 때문이다.

따라서 finally 블럭에서도 예외 처리를 해주어야 하며, 이는 코드의 복잡성을 증가시킨다.

 

public static void main(String[] args) {
        try{
            throw new RuntimeException("try 블록에서 발생한 예외");
        } finally {
            throw new RuntimeException("finally 블록에서 발생한 예외");
        }
    }

 

 

 

위 코드를 finally블럭에 다시 try-catch 문을 넣어서 예외처리를 해준 코드

public static void main(String[] args) {
        try{
            throw new RuntimeException("try 블록에서 발생한 예외");
        } finally {
            try{
                throw new RuntimeException("finally 블록에서 발생한 예외");
            } catch (Exception e){
                System.out.println(e.getMessage());
            }
        }
    }

 

즉 문제는,

1. finally블록에서는 자원해제를 담당해야하는데 필수불가결하게 예외처리도 신경써주어야 한다는 점..

2. 여러개의 자원을 처리할 때의 복잡도 증가

때문에 코드의 복잡성을 높이고 개발 생산성을 비롯한 문제가 생기게 됩니다.

 

이를 해결하기 위해 Java7 부터 도입된 try-with-resource.

 

try - with - resource의 등장!

try-with-resource를 사용하기 위해서는 해당 자원이 AutoCloseable 인터페이스를 구현해야합니다.

(Java7 개발 당시 기존 코드와의 호환을 위해 AutoCloseable인터페이스를 Closeable의 부모로 선언하였음)

 

자원회수는 try-with-resource에서 자동으로 담당하게 되며 실제 try 블록에서 발생한 예외에 대해 추적할 수 있다.

코드가 간단/명료 해지며 예외처리도 훨씬 간결해진다.

public static void main(String[] args) {
        // try-with-resources 구문 사용
        try (BufferedReader br = new BufferedReader(new FileReader("./Algorithm/day41/test.txt"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

 

 

한줄요약.

꼭 회수해야 하는 자원을 다룰 때는 try-finally 말고, try-with-resource를 사용하자.

코드는 더 짧고 정확하고 쉽게 자원을 회수할 수 있다.

'Java' 카테고리의 다른 글

[Java] JVM이 도대체 뭐야?!  (1) 2024.01.17
[Java] Thread와 동기화에 대해 알아보자.  (1) 2024.01.13