ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 17. 예외처리 (Exception, try-catch-finally) (2)
    자바(Java) 강의 2019. 5. 13. 10:26

    이 포스트에서는 자바의 예외처리 try-catch-finally 문에 대해 알아보도록 한다.

    이전 포스트

    목표

    • try-catch
    • try-catch-finally
    • finally 는 언제 사용할까?

    try-catch

    이전 포스트에서 try-catch를 이용하면 try블록 안에서 발생한 예외를 catch 블록에서 처리할 수 있다고 했다. 또한 try 블록안에서 예외가 나면 블록의 나머지 코드는 실행하지 않고 바로 catch로 넘어가는것을 확인했다. 이제 어떤 상황을 가정해보자. 
    Main 클래스에 main 메서드가 있다.
    public class Main {
    public static void main(String[] args) {
    String sentenceToCheck = null;
    StringChecker checker = new StringChecker();
    if(checker.endingWithSemicolon(sentenceToCheck)) {
    System.out.println("이 문장은 세미콜론으로 끝난다.");
    } else {
    System.out.println("이 문장은 세미콜론으로 끝나지 않는다.");
    }
    }
    }
    그리고 지난번과 마찬가지로 같은 패키지 내에 StringChecker 클래스가 있다.
    public class StringChecker {
    public boolean endingWithSemicolon(String sentence) {
    try {
    return sentence.endsWith(";");
    } catch (NullPointerException e) {
    System.out.println("예외 발생!");
    return false;
    }
    }

    private void mustRun() {
    System.out.println("이 메서드는 무조건 실행되어야 한다.");
    }
    }
    예외 발생!
    이 문장은 세미콜론으로 끝나지 않는다.
    목표는 mustRun이라는 메서드를 예외가 나든 나지 않든 마지막에 반드시 실행시키는 것이다. 어떻게 해야할까? 
    public class StringChecker {
    public boolean endingWithSemicolon(String sentence) {
    try {
    boolean isEndingWithSemicolon = sentence.endsWith(";");
    mustRun();
    return isEndingWithSemicolon;
    } catch (NullPointerException e) {
    System.out.println("예외 발생!");
    mustRun();
    return false;
    }
    }

    private void mustRun() {
    System.out.println("이 메서드는 무조건 실행되어야 한다.");
    }
    }
    첫번째 방법으로는 위처럼 return하기 마지막에 무조건 mustRun을 불러주는 방법이 있다. 위를 실행시키면 '이 문장은 세미콜론으로 끝나지 않는다.’가 출력되기 전에 '이 메서드는 무조건 실행되어야 한다.’가 실행되는 것을 확인할 수 있다. 이 방법의 단점은 무엇인가? 메서드를 두번이나 써 줘야 한다는 것이다. 또한 mustRun메서드를 실행하기 위해 isEndingWithSemicolon이라는 변수를 만들어 리턴해 주어야 한다. mustRun을 한번만 콜하는 다른 방법이 있을까? 한번 혼자 생각해보고 다음으로 넘어가길 바란다.

    public class StringChecker {
    public boolean endingWithSemicolon(String sentence) {
    boolean isEndingWithSemicolon = false;
    try {
    isEndingWithSemicolon = sentence.endsWith(";");
    } catch (NullPointerException e) {
    System.out.println("예외 발생!");
    }
    mustRun();
    return isEndingWithSemicolon;
    }

    private void mustRun() {
    System.out.println("이 메서드는 무조건 실행되어야 한다.");
    }
    }

    방법은 try 바깥에 isEndingWithSemicolon이라는 변수를 미리 false초기화 해 놓는 것이다. 그리고 try안에서 이 변수를 업데이트한다. 성공적으로 업데이트 된다면 try-catch바깥으로 나가 나머지 코드인 mustRun()을 실행하고 리턴할 것이다. 그렇지 않다면 catch구문으로 들어갈것이다. catch에서 예외처리를 했으니 try-catch바깥의 나머지 코드를 진행할 것이다. 이런 방법은 위의 방법보다 코드의 간결성 측면에서 더 자주 사용된다. 또 다른 방법이 있을까?

    try-catch-finally

    또 다른 방법은 try-catch-finally블록을 사용하는 것이다. finally블록은 try-catch 마지막에 반드시 실행된다. (예외, finally 실행 도중 쓰레드가 종료되는 경우 제외.)
    public class StringChecker {
    public boolean endingWithSemicolon(String sentence) {
    try {
    return sentence.endsWith(";");
    } catch (NullPointerException e) {
    System.out.println("예외 발생!");
    return false;
    } finally {
    mustRun();
    }
    }

    private void mustRun() {
    System.out.println("이 메서드는 무조건 실행되어야 한다.");
    }
    }
    finally는 보통 catch 블록 다음에 사용한다. 무조건 catch 다음에 사용할 필요는 없다 try { } finally { }도 가능하다. 하지만 보통 try {} catch {} finally {}로 많이 사용하므로 여기서는 try-catch-finally로 설명을 하겠다.



    위의 이미지는 정상 동작의 경우 블록이 실행되는 순서와 예외가 발생하는 경우 블록이 실행되는 순서를 나타낸 다이어그램이다. 정상 동작의 경우 예외가 발생하지 않으므로 try블록을 모두 끝마친 후 finally블록을 실행한다. 예외가 발생하는 경우는 try실행 도중 catch로 넘어가고 catch가 끝난 후 finally를 실행한다.
     finally는 try-catch블록의 마지막에 실행되는 것이지 메서드의 마지막에 실행되는것이 아니란 점을 명심하자. 무슨 뜻인지 모르겠다면 아래의 예를 보자.

    public class StringChecker {
    public boolean endingWithSemicolon(String sentence) {
    try {
    return sentence.endsWith(";");
    } catch (NullPointerException e) {
    System.out.println("예외 발생!");
    } finally {
    mustRun();
    }

    try {
    System.out.println("또 다른 try-catch");
    } finally {
    System.out.println("또 다른 finally");
    }
    return false;
    }

    private void mustRun() {
    System.out.println("이 메서드는 무조건 실행되어야 한다.");
    }
    }
    예외 발생!
    이 메서드는 무조건 실행되어야 한다.
    또 다른 try-catch
    또 다른 finally
    이 문장은 세미콜론으로 끝나지 않는다.

    위의 예제가 보여주듯 finally는 자신이 속한 try-catch의 마지막에 실행되는 것이지 메서드의 마지막에 실행되는게 아니다. 처음 try-catch-finally를 접하는 경우 헷갈릴 수 있다. 
     하지만 이 방법이 두번째에서 했던 mustRun을 리턴 전에 콜하는 방법과 뭐가 다른가? finally를 사용하지 않고도 충분히 해결 가능한데 왜 굳이 finally를 사용하는가?

    public class StringChecker {
    public boolean endingWithSemicolon(String sentence) {
    try {
    return sentence.endsWith(";");
    } catch (NullPointerException e) {
    System.out.println("예외 발생!");
    int zero = 0;
    int dividedByZero = 3/zero;
    return dividedByZero > 1;
    } finally {
    mustRun();
    }
    }

    private void mustRun() {
    System.out.println("이 메서드는 무조건 실행되어야 한다.");
    }
    }

    예외 발생!
    이 메서드는 무조건 실행되어야 한다.
    Exception in thread "main" java.lang.ArithmeticException: / by zero
    at StringChecker.endingWithSemicolon(StringChecker.java:10)
    at Main.main(Main.java:6)

    위의 코드는 어디에서 예외가 발생했는가? catch문 안에서 또 다른 예외가 발생했다. 그럼에도 불구하고 finally블록이 실행됐다. 이처럼 finally블록은 try/catch안에서 어떤 예외가 나더라도 마지막에 실행을 해준다. 만약 메서드의 마지막에서 mustRun을 부르고 리턴하는 구조로 짰다면 catch에서 예외 발생시 mustRun을 실행하지 않고 예외가 메인 메서드로 전파되었을 것이다.

    finally는 언제 사용할까?

    이렇게까지 간절하게 무언가를 실행해야하는 경우는 언제일까? finally는 보통 IO(Input/Output) 오퍼레이션 또는 Synchronization(Lock)에서 많이 사용한다. IO오퍼레이션에는 파일입출력, 네트워크 커넥션, 데이터베이스 커넥션 등이 있다. 파일/네트워크/데이터베이스 등 어떤 리소스를 사용하기 위해 open(또는 connect)이라는 메서드를 사용한다. 프로그램은 open한 리소스를 사용이 끝난 후 close해야 한다. 그렇지 않으면 리소스를 제공한 입장(운영체제/서버)에서는 이 리소스는 누군가 사용중이고 close를 하지 않았으므로 이 리소스를 필요로 하는 다른 프로그램에게 제공할 수 없다. 

    Connection conn = null;
    try {
    conn = DriverManager.getConnection(databaseURL, user, password);
        // some other logic here
    } catch (SQLException e) {
        // handling the exceptions
    } finally {
    conn.close();
    }

    예를들어서 위처럼 데이터베이스에 연결하는 경우 데이터베이스를 이용한 후 conn.close()를 이용해 데이터베이스 커넥션을 닫아주어야한다. 
     아직 컴퓨터를 공부중이라면 동기화를 예로 드는것이 더 이해하기 쉬울 수 있다. Lock의 예를 보자.
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;

    public class SynchronizableClass {
    Lock lock = new ReentrantLock();
    public void runWithCriticalSection() {
    lock.lock();
    try {
    // critical section
    } catch (NullPointerException e) {

    } finally {
    lock.unlock();
    }
    }
    }
    위처럼 임계영역인 try catch 부분을 lock하기 위해 lock()메서드를 부르면 임계영역(critical section)의 실행이 끝난 후 해당 lock을 놓아주어야 한다. 그렇지 않으면 다른 쓰레드가 이 메서드를 실행하려 하는 경우 unlock이 되지 않았기 때문에 실행하지 못하고 계속 기다려야 한다. 이런 경우 어떤 이유에서 이 메서드에서 예외/에러가 발생해 unlock이 실행되지 않고 메서드가 종료되면 프로그램은 데드락(Deadlock)상태에 빠지고 만다. 이를 방지하기 위해 finally 블록 안에서 unlock을 부른다.

    입출력 오퍼레이션(IO Operation)의 경우 자바 8에서는 개발자가 finally를 부르지 않아도 알아서 입출력 오브젝트를 닫아주는 문법이 추가되었다. 이 문법은 이후에 입출력을 설명하면서 소개하도록 하겠다.

    연습문제

    try 문 안에 또 try 문을 넣을 수 있다. 이중으로 try를 넣는 경우 예외 발생시 어떤 순서로 프로그램이 실행될지 예측할 수 있는가?


    public class StringChecker {
    public boolean endingWithSemicolon(String sentence) {
    try {
    System.out.println("endingWithSemicolon 연산 시작.");
    try {
    System.out.println("Arithmetic 연산 시작.");
    int zero = 0;
    int dividedByZero = 3/zero;
    System.out.println("Arithmetic 연산 종료.");
    return dividedByZero > 1;
    } catch (ArithmeticException e) {
    System.out.println("ArithmeticException 발생!");
    } finally {
    System.out.println("ArithmeticException finally 실행.");
    }
    System.out.println("endingWithSemicolon 연산 리턴 시도.");
    return sentence.endsWith(";");
    } catch (NullPointerException e) {
    System.out.println("endingWithSemicolon 예외 발생!");
    return false;
    } finally {
    mustRun();
    }
    }

    private void mustRun() {
    System.out.println("이 메서드는 무조건 실행되어야 한다.");
    }
    }

    다음 포스트: 17. 예외처리 (Exception, throw and throws) (3)


    댓글

f.software engineer @ All Right Reserved