Java

[Java] try나 catch 블록에서 return 하면 finally 블록이 실행이 될까?

feelcoding 2022. 9. 29. 01:23
728x90

try~catch문에서 finally 블럭은 도대체 왜 필요한걸까?

try 블럭에서 예외가 발생하지 않으면 try-catch문 이후에 있는 코드가 실행이 될거고

try 블럭에서 예외가 발생해서 catch 블럭으로 갔다고 해도 가장 큰 범위인 Exception을 잡는 catch 블럭이 마지막에 무조건 있다면 어떤 예외가 발생하더라도 catch 블럭에 걸릴테니까 그 안에서 예외처리를 해주면 try-catch문 이후에 있는 코드가 실행이 될텐데

왜 굳이 finally가 필요할까? 하는 생각이 문득 들었다.

그래서 finally에 대해서 찾아보게 되었다.

 

일단 아래의 코드를 보자.

public class FinallyTest {
    public static void main(String[] args) {
        FinallyTest finallyTest = new FinallyTest();
        finallyTest.returnInFinally();
    }

    public void returnInFinally() {
        try {
            int n = 5 / 0;
            System.out.println("여기는 try 블럭");
        } catch (Exception e) {
            System.out.println("여기는 catch 블럭");
        } finally {
            System.out.println("여기는 finally 블럭");
        }
    }
}

이 코드의 결과는 어떻게 될까?

 

5를 0으로 나누는 순간 예외가 발생하여 catch 블럭으로 들어가게 되므로

System.out.println("여기는 try 블럭");

 위 코드는 실행되지 않고 catch 블럭으로 들어가게 된다.

catch블럭에서 "여기는 catch 블럭"을 출력하고 finally 블럭으로 들어가서 "여기는 finally 블럭"을 출력한다.

따라서 위 코드의 실행 결과는 아래와 같다.

여기는 catch 블럭
여기는 finally 블럭

 

그럼 이제 try 블럭과 catch 블럭에 return문을 넣어보자.

public class FinallyTest {
    public static void main(String[] args) {
        FinallyTest finallyTest = new FinallyTest();
        finallyTest.returnInFinally();
    }

    public void returnInFinally() {
        try {
            int n = 5 / 0;
            System.out.println("여기는 try 블럭");
            return;
        } catch (Exception e) {
            System.out.println("여기는 catch 블럭");
            return;
        } finally {
            System.out.println("여기는 finally 블럭");
        }
    }
}

위 코드의 실행 결과는 어떻게 될까?

1. catch 블럭에서 return을 해줬으니까 finally는 실행이 되지 않을 것이다.

2. return문을 만났어도 finally 블럭은 실행될 것이다.

.

.

.

.

.

.

.

정답은 2번이다.

위 코드를 실행하면 실행 결과는 아래와 같다.

여기는 catch 블럭
여기는 finally 블럭

try 블럭이 실행되고 나서 finally 블럭이 최종적으로 실행된다.

 

그러면 아래의 코드는 실행결과가 어떻게 될까?

public class FinallyTest2 {
    public static void main(String[] args) {
        FinallyTest2 finallyTest = new FinallyTest2();
        finallyTest.returnInFinally();
    }

    public void returnInFinally() {
        while (true) {
            try {
                System.out.println("여기는 try 블럭");
                break;
            } catch (Exception e) {
                System.out.println("여기는 catch 블럭");
            } finally {
                System.out.println("여기는 finally 블럭");
            }
        }
    }
}

while문을 들어가자마자 break;를 해줬다.

1. break문을 만났으니 바로 while문을 빠져나갈 것이다.

2. break문을 만났어도 finally 블럭은 실행될 것이다.

.

.

.

.

.

.

정답은 2번이다.

위 코드의 실행 결과는 아래와 같다.

여기는 try 블럭
여기는 finally 블럭

 

그러면 아래의 경우에는 어떨까?

public class FinallyTest3 {
    public static void main(String[] args) {
        try {
            FinallyTest3 finallyTest = new FinallyTest3();
            finallyTest.returnInFinally();
        } catch (Exception e) {
            System.out.println("여기는 main 메소드의 catch 블럭");
        }
    }

    public void returnInFinally() {
        try {
            int n = 5 / 0;
            System.out.println("여기는 try 블럭");
        } catch (Exception e) {
            System.out.println("여기는 catch 블럭");
            throw e;
        } finally {
            System.out.println("여기는 finally 블럭");
        }
    }
}

catch 블럭에서 잡은 예외 객체를 그대로 다시 던져줬다.

그렇다면 이 코드의 실행 결과는 어떻게 될까?

1. Java의 메소드에서 예외가 발생하면 바로 메소드를 빠져나가므로 finally 블럭은 실행되지 않을 것이다.

2. 예외가 발생해도 메소드를 바로 빠져나가지 않고 finally 블럭이 실행될 것이다.

.

.

.

.

.

.

.

정답은 2번이다.

위 코드의 실행 결과는 아래와 같다.

여기는 catch 블럭
여기는 finally 블럭
여기는 main 메소드의 catch 블럭

 

그럼 마지막으로 아래의 코드를 보자.

public class FinallyTest4 {
    public static void main(String[] args) {
        try {
            FinallyTest4 finallyTest = new FinallyTest4();
            int result = finallyTest.returnInFinally();
            System.out.println("result: " + result);
        } catch (Exception e) {
            System.out.println("여기는 main 메소드의 catch 블럭");
        }
    }

    public int returnInFinally() {
        try {
            int n = 5 / 0;
            System.out.println("여기는 try 블럭");
            return 0;
        } catch (Exception e) {
            System.out.println("여기는 catch 블럭");
            throw e;
        } finally {
            System.out.println("여기는 finally 블럭");
            return 1;
        }
    }
}

앞의 코드에서 catch 블럭에서 예외가 발생한 경우에도 finally 블럭이 실행되었다고 했으니 finally 블럭의 "여기는 finally 블럭"이 출력될 것이다.

그러면 메인 메소드에서 catch 블럭에 걸려서 "여기는 main 메소드의 catch 블럭"을 출력할까? 아니면 result: 1을 출력할까?

1. returnInFinally 메소드에서 예외를 던졌으므로 main 메소드의 catch 블럭이 실행될 것이다.

2. finally에서 1을 return 했으니 main 메소드에서 정상적으로 1을 return 받고 catch 블럭에 들어가지 않는다.

.

.

.

.

.

.

.

.

정답은 2번이다.

위 코드의 실행 결과는 다음과 같다.

여기는 catch 블럭
여기는 finally 블럭
result: 1

 

https://docs.oracle.com/javase/tutorial/essential/exceptions/finally.html

 

The finally Block (The Java™ Tutorials > Essential Java Classes > Exceptions)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

위 링크는 Java의 공식 튜토리얼 사이트인데

예상치 못한 예외가 발생한 경우에도 finally 블럭이 실행되는 것을 보장한다.

return, continue, break로 인해 cleanup 코드가 실행되지 않는 일을 막을 수 있다.

라고 나와있다.

 

그러니 이 글의 초반에서 finally가 왜 필요할까? 하는 의문이 해소되었다.

return, continue, break, 예외 발생 등으로 메소드가 종료되어도 finally 블럭의 코드는 실행된다. (return 이후에 finally 블록이 실행된다)

return, break 등을 만나 메소드나 블록을 빠져나가는 경우에도 무조건 실행되어야 하는 코드가 있는 경우 매우 유용할 것 같다.

 

그런데 조금 놀란 것은 마지막 코드(FinallyTest4)에서 finally에서 return을 하는 바람에 앞에서 발생했던 예외가 무시되었다는 것이다.

하나의 메소드에서는 return과 예외를 던지기 2개를 모두 할 수 없다.

return을 하거나 예외를 던지거나 둘 중 하나만 가능하다.

 

위의 코드에서는 catch 블럭에서 예외를 throw 했으나 메소드 종료 전에 finally 블럭이 실행되면서 예외를 throw 하는 것이 아니라 1을 return 하게 되었다. finally문에서 return을 하는 바람에 throw한 Exception을 잃어버리게 된 것이다.

catch 블럭에서 예외를 던진 것이 아니라 return을 한 경우에도 해당 return 값은 무시되고 finally에서 return 한 값이 최종적으로 return 될 것이다.

try문에서 예외가 발생하지 않아 return 0; 부분이 실행되었더라도 0을 반환하는 것이 아니라 finally문에 걸려서 1을 반환할 것이다.

 

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

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

finally 블럭 안에서 return을 하는 것은 의도했다 하더라도 던져진 예외를 가릴 수 있고 디버깅을 어렵게 할 수 있다.

이제 finally 블럭 안에서 return을 하는 것은 안 된다는 것은 알았다.

그러면 아래와 같은 코드는 어떨까?

try 블럭 이후에 finally 블럭이 실행될텐데 5를 반환할까? 10을 반환할까?

public class FinallyTest5 {
    public static void main(String[] args) {
        FinallyTest5 finallyTest = new FinallyTest5();
        int result = finallyTest.returnInFinally();
        System.out.println("result: " + result);
    }

    public int returnInFinally() {
        int n = 5;
        try {
            System.out.println("여기는 try 블럭");
            return n;
        } finally {
            System.out.println("여기는 finally 블럭");
            n = 10;
        }
    }
}

위 코드의 실행결과는 아래와 같다.

여기는 try 블럭
여기는 finally 블럭
result: 5

finally 블럭에서 n을 10으로 바꿔줘도 5를 반환한다.

인텔리제이에서도 finally 블럭에서 n에 10을 할당해주는 부분에서 n은 회색으로 표시가 되고 아래와 같은 경고 메시지가 뜬다.

그럼 아래와 같이 참조자료형의 경우는 어떻게 될까?

public class FinallyTest6 {
    public static void main(String[] args) {
        FinallyTest6 finallyTest = new FinallyTest6();
        StringBuilder result = finallyTest.returnInFinally();
        System.out.println("result: " + result);
    }

    public StringBuilder returnInFinally() {
        StringBuilder str = new StringBuilder("five");
        try {
            System.out.println("여기는 try 블럭");
            return str;
        } finally {
            System.out.println("여기는 finally 블럭");
            str.setLength(0);
            str.append("ten");
        }
    }
}

StringBuilder 객체의 특정 인스턴스 변수를 "five"로 초기화했다가 try문에서 해당 객체를 return 하고 그 이후 finally에서 해당 필드의 값을 "ten"으로 바꿔줬다. (객체를 새로 생성해서 재할당 한 것이 아니라 객체는 그대로고 특정 필드의 값만 바꿨다.)

이 경우 실행 결과는 아래와 같다.

여기는 try 블럭
여기는 finally 블럭
result: ten

finally 블럭에서 값을 바꾼 것이 영향을 주었다.

 

인텔리제이에서도 finally 블럭에서 str이 회색 처리가 되지 않았다. (물론 finally 블럭에서 str = new StringBuilder("ten"); 이렇게 재할당을 했다면 str도 회색 처리가 되었을 것이다)

 

FinallyTest5처럼 기본 자료형인 경우와 FinallyTest6처럼 참조 자료형인 경우는 또 다르다.

return도 Java의 pass by value와 똑같다고 보면 된다.

이 사실을 염두에 두고 코드를 작성할 때 주의하면 좋을 것 같다.

 

그럼 finally 블록이 실행되지 않는 경우는 언제일까?

try 블럭이나 catch 블럭에서 이렇게 System.exit()을 하는 경우에는 finally 블럭이 실행되지 않는다.

System.exit(1);

 

System.exit() 메소드를 호출하면 아예 이 프로그램 자체가 종료되기 때문이다.

finally가 실행되지 않는 경우는 이 경우가 유일하다.

 

 

출처

https://stackoverflow.com/questions/3861522/do-you-really-need-the-finally-block

https://docs.oracle.com/javase/tutorial/essential/exceptions/finally.html

https://stackoverflow.com/questions/48088/returning-from-a-finally-block-in-java

https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.20.2

https://milkye.tistory.com/367

https://stackoverflow.com/questions/7297937/does-java-return-by-reference-or-by-value

https://stackoverflow.com/questions/4205362/java-beginner-returning-an-object-is-it-returned-as-a-constant-reference-o

728x90