Skip to content

Java의 예외처리

예외 처리란 무엇인가?

프로그램을 실행할 때 사용자가 통제할 수 없는 영역을 처리할 수 있게 도와줄 수 있는 것이 예외 처리이다. 예로들어 인터넷 자료를 다운 받아 처리하는 어플리케이션이 있다고 가정할 때, 인터넷 연결이 끊기면 어떻게 처리할 것인지 예외처리를 통해 정의할 수 있다.

예외처리의 예
// ...
try {
    인터넷자료받기();
} catch (네트워크통신예외발생 메시지) {
    적절한예외처리();
}
//...

예외 처리가 되면 어떤 일이 발생하는가?

예외처리가 되면 예외가 발생한 코드를 적절히 처리한 후 정상적으로 코드가 실행된다. 아래의 예제를 통해 예외처리를 할 때 어떤 일이 발생하는지 살펴보자

ExceptionExample.java
class SomeTask {
    public static void method1() {
        try {
            method2();
            System.out.println("method1에서 실행되지 않는 구문");
        } catch (NullPointerException error) {
            error.printStackTrace();
            System.out.println("예외처리 하였음");
        }
        System.out.println("method1에서 실행되는 구문");
    }

    private static void method2() {
        String emptyString = null;

        //예외 발생 부분
        emptyString.length();

        System.out.println("method2에서 실행되지 않는 구문");

    }
}


public class ExceptionExample {
    public static void main(String[] args) {
        SomeTask.method1();
        System.out.println("main 코드가 실행되는 부분");
    }

}

ExampleException.java 결과
java.lang.NullPointerException: Cannot invoke "String.length()" because "emptyString" is null
        at exception.SomeTask.method2(ExceptionExample.java:17)
        at exception.SomeTask.method1(ExceptionExample.java:6)
        at exception.ExceptionExample.main(ExceptionExample.java:26)
예외처리 하였음
method1에서 실행되는 구문
main 코드가 실행되는 부분
하이라이트 표시가 되어있는 부분에서 예외를 발생시켰다. 그 결과, method1() 메소드 내 method2()를 처리하다가. catch 부분의 코드가 실행됬음을 볼 수 있다. 이를 예외처리라고 한다.

그 이후 method1();는 정상 종료가 되고 main까지 정상종료가 되었음을 볼 수 있다. 이렇게 예외처리는 예외가 발생할 수 있는 상황을 적절히 처리하여 코드를 계속 동작할 수 있도록 만들게 된다.

예외처리를 하지 않으면 어떻게 되는가?

예외처리를 하지 않으면, 프로그램이 강제 종료가 되어 동작하지 않는다. 필자의 경험을 나누자면, 보안 뉴스 클롤링 하는 스크립트를 개발하게 되었는데, 인터넷이 끊기거나, 생각하지 못한 양식을 처리하지 못하게 되면 그대로 오류가 발생하여 종료가 되는 상황이 있었다!

10 분동안 자료를 처리하다가 중간에 오류가 발생하여 처음부터 다시 실행해야한다고 하면? 10분이 아니라 1시간짜리 처리를 하다가 예외상황이 발생하면? 시간 비용이 많이 소모가 될 것이고 문제해결을 할 수 없게 될 것이다. 예외처리 하나로 예외가 발생할때, 인터넷에 재 연결을 하거나, 특정 자료만 수집하지 않기로 변경하다면, 잃는 것 보다 얻는게 훨씬 더 많을 것이다.

자바의 예외 처리 방법

자바의 예외처리의 전형적인 패턴인 try-catch 구문이 있고 java 7부터 지원이 된, try-with-resource가 추가되어서 이와 함께 소개하고자 한다.

try-catch

전통적인 try-catch 방법이다. 예외가 발생할 때 직접 처리하기 위해 쓰인다. 문법은 아래와 같다.

TryCatch.java
try {
    코드 입력
} catch([처리할 예외 종류] 예외 객체) {
    예외 처리 코드 작성
}

이를 넘어 이런식으로 다른 예외를 |(OR 연산)하여 코드를 더욱 간결하게 만드는 방식도 있으니 참고 사항으로 기억하면 좋다.

TryCatch2.java
try {
    코드 입력
} catch(예외처리1 | 예외처리2 | ... 예외 객체) {
    예외 처리 코드 작성
}

throws

throws는 예외가 발생하는 상황이 있을 때, 직접 처리하지 않는다. 사용법은 아래와 같이 메소드 명 뒤에 throws 예외처리명을 작성하면 된다.

ExampleException 예제를 변형하여 method1()throws를 사용하여 method1()을 사용하는 main에서 예외처를 대신 처리하도록 요청한다.

ExceptionExample2.java
class SomeTask {
    public static void method1() throws NullPointerException {
        method2();
        System.out.println("method1에서 실행되는 구문");
    }

    private static void method2() {
        String emptyString = null;

        // 예외 발생 부분
        emptyString.length();

        System.out.println("method2에서 실행되지 않는 구문");

    }
}


public class ExceptionExample2 {
    public static void main(String[] args) {
        try {
            SomeTask.method1();
        } catch (NullPointerException error) {
            error.printStackTrace();
            System.out.println("예외처리 하였음");
        }
        System.out.println("main 코드가 실행되는 부분");
    }

}

ExceptionExample2 실행 결과
java.lang.NullPointerException: Cannot invoke "String.length()" because "emptyString" is null
        at exception.SomeTask.method2(ExceptionExample2.java:13)
        at exception.SomeTask.method1(ExceptionExample2.java:5)
        at exception.ExceptionExample2.main(ExceptionExample2.java:24)
예외처리 하였음
main 코드가 실행되는 부분

try-with-resource

try-with-resource는 프로그램이 외부 자원을 사용할 때 예외처리하기 편하게 하도록 만든 java7 부터 생긴 문법이다. 외부 자원이란, 사용자의 입력, 파일 입출력, 네트워크 소켓 등이 해당된다.

외부 자원을 이용하는 도중 예외 발생 시 java는 사용하고 있는 자원들을 자동으로 할당 해제를 할 수 없기 때문에 예외 처리 시 꼭 close() 메소드를 통해 해제를 해야 한다.

기존 try-catch구문을 사용하게 되면 일일히 catch 구문에 close() 메소드가 도배될 수 있는 문제점이 있고, 이는 코드를 관리하는데 어려움을 겪게 된다.

try-with-resource 사용 방법

사용 방법은 아래처럼 작성 가능하며, try 블록에서 예외 발생 시 자동으로 할당 해제가 되고, 코드도 간결해진다.

ExceptionExample3.java
public class ExceptionExample3 {
    public static void main(String[] args) {
        try (Scanner scanner = new Scanner(System.in)) {
            System.out.print("input your name : ");
            String name = scanner.nextLine();
            System.out.println("your name is : " + name);
        }
    }
}

try-with-resource 사용 조건

try-with-resource는 AutoCloseable 인터페이스 및 하위 인터페이스를 구현(implement)한 클래스만 사용 가능하다. Scanner 인터페이스를 살펴보면 implements Closeable 이 존재하고, CloseableAutoCloseable의 하위 인터페이스다. 이에 따라 try-with-resource를 사용할 수 있다.

Scanner.java
public final class Scanner implements Iterator<String>, Closeable {
...
}

try-with-resource의 동작을 자세히 알기 위해 직접 AutoCloseable을 구현한 예제는 아래와 같다. try안에 있는 코드를 실행한 후 close() 구문이 실행 되고, main 코드가 마무리 됨을 확인할 수 있다.

ExceptionExample4.java
public class ExceptionExample4 {
    public static void main(String[] args) {
        try (MyAutoCloseable closeable = new MyAutoCloseable()) {
            System.out.println("코드 실행");
        }
        System.out.println("main 코드 마무리");
    }

}


class MyAutoCloseable implements AutoCloseable {
    public MyAutoCloseable() {

    }

    @Override
    public void close() throws RuntimeException {
        System.out.println("자동으로 끝낼게요!");
    }

}
ExceptionExample4 실행 결과
코드 실행
자동으로 끝낼게요!
main 코드 마무리

finally 코드

finally는 예외처리와 상관 없이 무조건 실행되어야하는 코드를 작성하는데 도움이 된다. 아래는 finally를 사용하는 예제이다. NullPointerException이 발생하더라도 코드가 실행되었음을 확인할 수 있다.

finally를 적용하는것과 try-catch 밖에서 작성하는것과 필자가 보이게는 크게 차이가 나지 않기 때문에 사용성은 그렇게 좋지 않다고 생각한다. finally를 무조건 적용해야하는 상황이 있다면 별도의 블로그로 올려보겠다.

ExceptionExample5.java
public class ExceptionExample5 {
    public static void main(String[] args) {
        SomeTask.method1();
        System.out.println("main 코드가 실행되는 부분");
    }

}


class SomeTask {
    public static void method1() {
        try {
            method2();
            System.out.println("method1에서 실행되지 않는 구문");
        } catch (NullPointerException error) {
            error.printStackTrace();
            System.out.println("예외처리 하였음");
        } finally {
            System.out.println("꼭 실행되어야 함.");
        }
        System.out.println("method1에서 실행되는 구문");
    }

    private static void method2() {
        String emptyString = null;

        //예외 발생 부분
        emptyString.length();

        System.out.println("method2에서 실행되지 않는 구문");

    }
}
ExceptionExample5 실행 결과
java.lang.NullPointerException: Cannot invoke "String.length()" because "emptyString" is null
        at exception.SomeTask.method2(ExceptionExample6.java:33)
        at exception.SomeTask.method1(ExceptionExample6.java:18)
        at exception.ExceptionExample6.main(ExceptionExample6.java:8)
예외처리 하였음
꼭 실행되어야 함.
method1에서 실행되는 구문
main 코드가 실행되는 부분

자바의 예외종류

자바의 예외종류는 매우 많다. 제일 최상단의 클래스는 Throwable이며, Error와 Exception으로 나뉘어지게 된다.

Error는 프로그래머가 건드릴 수 없는 OS 자원과 같은 문제이므로 프로그래머가 직접 사용하지 않는 오류이다.

Exception은 프로그래머가 제어하는 가장 상단의 예외처리이고, 하위로는 checked-exception unchecked-exception이 있다.

checked-exception

checked exception은 예외 발생 시 프로그래머가 반드시 처리해야하는 익셉션이다. 무슨 이야기냐면, 하나의 메소드가 throws를 통해 checked-exception을 던지게 되면 개발자는 무조건 해당 예외에 대해 처리를 해야한다. 그렇지 않으면 컴파일 오류가 발생하게 된다.

checked-exception은 Exception 및 RuntimeException을 제외한 Exception의 하위 클래스이며 공식 문서를 통해 확인 가능하다. checked-exception 중 AlreadyBoundException를 사용해보자.

method1에 throws AlreadyBoundException를 추가하게 되면, 아래와 같은 오류가 발생하게 되면서 컴파일 자체를 할 수 없게 된다. 이는 checked-exception을 사용할 때 예외처리를 하지 않아서 컴파일 단계에서 오류를 발생시킨 것이다.

Unhandled exception type AlreadyBoundException

이를 문법에 맞춰 수정하기 위해서는. 사용하는 메소드에 AlreadyBoundException을 throws를 하거나, try-catch를 이용해 직접 처리할 수 밖에 없다. method1()에서 AlreadyBoundException을 사용하게 되면서, main코드를 실행시키기 위해 throws 구문으로 예외를 던젔다. main에서 throws를 하게 되면, JVM이 대신 처리하게 된다고 한다.

ExceptionExample6.java
public class ExceptionExample6 {
    public static void main(String[] args) throws AlreadyBoundException {
        SomeTask.method1();
        System.out.println("main 코드가 실행되는 부분");
    }

}


class SomeTask {
    public static void method1() throws AlreadyBoundException{
        try {
            method2();
            System.out.println("method1에서 실행되지 않는 구문");
        } catch (NullPointerException error) {
            error.printStackTrace();
            System.out.println("예외처리 하였음");
        } finally {
            System.out.println("꼭 실행되어야 함.");
        }
        System.out.println("method1에서 실행되는 구문");
    }

    private static void method2() {
        String emptyString = null;

        //예외 발생 부분
        emptyString.length();

        System.out.println("method2에서 실행되지 않는 구문");

    }
}

unchecked-exception

그럼 모든 예외를 사용하기 위해서는 위처럼 throws를 해야 할까? 결론부터 말하자면 아니다. unchecked-exception은 프로그램을 실행할 때 예측이 불가능한 상황 (값이 null이거나, 0으로 나누게 되는 상황 등)을 처리할 때 checked exception처럼 예외를 직접 처리하도록 강제하지 않는다.

이미 예상했겠지만, ExceptionExample2에서 method1() 안에 NullPointerException을 throws로 넘겨도 컴파일 오류가 발생하지 않는 것을 알 수 있다.

unchecked-exception의 최상위 클래스는 RuntimeException이고, 하위 클래스는 공식 문서에서 찾아볼 수 있다.

사용자 정의 예외 생성

사용자 정의 예외를 만들 수도 있다. 예외를 관리하도록 직접 객체를 생성하여 이에따라 처리하도록 구성하려면 아래 처럼 원하는 예외를 상속받으면 된다.

아래의 예제코드에서 method1()에서 NullPointerException 예외를 처리할 때 커스텀 예외(CustomException)를 발생시켰고, main()에서 예외가 발생이 되었다. 따라서, "main 코드가 실행되는 부분"은 예외로 인해 실행되지 않게 되었다.

ExceptionExample7.java
class ExceptionExample7 {
    public static void main(String[] args){
        SomeTask.method1();
        System.out.println("main 코드가 실행되는 부분");
    }

}

class CustomException extends NullPointerException {
    String prefix = "[ERROR] CustomException 발생! ";
    public CustomException(String message){
        super(message);
    }
    @Override
    public String getMessage() {
        String message = super.getMessage();
        return  prefix + message;
    }
}


class SomeTask {
    public static void method1(){
        try {
            method2();
            System.out.println("method1에서 실행되지 않는 구문");
        } catch (NullPointerException error) {
            throw new CustomException("NullPointer 오류가 발생하였습니다!");
        }
        System.out.println("method1에서 실행되는 구문");
    }

    private static void method2() {
        String emptyString = null;

        //예외 발생 부분
        emptyString.length();
    }
}
ExceptionExample7 실행 결과
Exception in thread "main" exception.CustomException: [ERROR] CustomException 발생! NullPointer 오류가 발생하였습니다!
        at exception.SomeTask.method1(ExceptionExample7.java:30)
        at exception.ExceptionExample7.main(ExceptionExample7.java:17)

결론

예외처리는 예상하지 못한 상황이 있을 때 적절히 통제할 수 있는 방식을 제공해주지만, 프로그램의 복잡성이 늘어남에 따라 전체를 통제할 수 없다. 버그가 발생하여 프로그램 동작이 완전히 중지되지 않게 해주지만, 완벽하게 버그를 통제할 수는 없다. 따라서 예외처리를 할 경우에는 예외가 발생한 위치와, 무엇때문에 예외가 발생하였는지, 이를 해결하기 위해 어떻게 해야하는지에 대한 정보를 충분히 남겨야 한다고 생각한다.

Comments