9 min read

<Java> 예외처리

프로그램 오류의 종류

본격적으로 예외를 공부하기 전에 예외가 발생하는 상황인 오류,즉 에러의 종류부터 먼저 짚고 넘어가겠습니다.프로그램이 실헹 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우의 해당 원인을 에러라고 정의합니다.

이는 발생 시점에 따라 “컴파일 에러”“런타임 에러”로 나눌 수 있습니다.컴파일 에러의 경우 흔히 IDE가 빨간줄을 띄워주면서 개발자에게 알려주는 에러입니다.반면 런타임 에러의 경우 프로그램을 실행시킨다음 발생하는 에러로 해당 로그를 보며 디버깅을 통해 오류를 찾아내야 합니다.

이중 런타임 에러를 방지하기 위해 자바에서 해당 에러를 두가지 경우로 나누어 관리하고 있습니다.바로 error와 exception입니다.에러는 메모리 부족이나 스택오버플로우와 같이 일단 발생하면 복구할 수 없는 심각한 에러인 반면에 예외는 발생하더라도 수습 가능한 정도의 수준이라고 보시면 됩니다.

예외의 경우 발생하더라도 개발자가 이에 대한 적절한 코드를 미리 작성해 놓으면 처리가 가능합니다.

예외 클래스의 계층구조

앞서 말한 에러와 예외는 위와 같은 계층구조를 가지며 자바 내에서 클래스로 정의되어 있습니다.앞서 배운 바와 같이 모든 클래스의 조상은 Object임을 확인할 수 있습니다.

모든 예외의 최고 조상은 Exception 클래스이며 위와 같이 예외의 경우 크게 두가지 분류로 나눌 수 있습니다.(RuntimeException와 자식들,그외 Exception의 자식들)

예외 처리하기(try-catch)

프로그램의 실행 도중에 발생하는 에러는 어쩔 수 없지만,예외는 개발자가 이에 대한 처리를 미리 해주어야 합니다.예외 처리(Exception Handling)란 프로그램 실행 시 발생할 수 있는 예기치 모한 예외의 발생에 대비한 코드를 작성하는 것입니다.이로써 프로그램이 갑자기 종료되는 불상사를 막는 동시에 지속적으로 프로그램을 실행 시킬 수 있게 합니다.예외를 처리하기 위해 try-catch 구문을 사용하며,다음 예제를 통해 알아봅시다.

public class ExceptionEx1 {
    public static void main(String[] args) {
        int num = 100;
        int result = 0;

        for (int i = 0; i < 10; i++) {
            try {
                result = num / (int) (Math.random() * 10);
                System.out.println(result);
            } catch (Exception e) {
                String message = e.getMessage();
				e.printStackTrace();
                System.out.println("message = " + message);
            }
        }
    }
}

위의 코드는 0부터 9까지 숫자 중 랜덤으로 나온 숫자로 100이라는 숫자를 나누는 예제입니다.즉,random값에 의해 0이 나오게 될 경우 ArithmeticException을 발생시킵니다.하지만 try-catch를 통해서 예외 처리를 진행해주었기에 error가 발생하지 않고 아래와 같은 출력을 확인해 볼 수 있습니다.0으로 숫자를 나누게 될 경우 해당 예외의 메세지를 출력합니다.(try구문에서 예외를 생길수 있고 만약 예외가 생길경우,catch에서 해당 예외를 잡는다고 생각하셔도 됩니다)

예외 발생시키기(throw)

위의 예제를 통해 예외가 발생할수 있는 경우를 대비하여 예외처리를 진행했습니다.이번에는 개발자가 직접 정의한 Exception을 한번 발생시켜보겠습니다.

public class ExceptionEx2 {
    public static void main(String[] args) {
        try {
            throw new Exception("my exception");
        } catch (Exception e) {
            e.printStackTrace();
            String message = e.getMessage();
            System.out.println("message = " + message);
        }
    }
}

위와 같이 new 연산자를 통해 발생시키려는 예외 클래스의 객체를 만든 다음 키워드 throw를 통해서 예외를 발생시키면 됩니다.throw 키워드는 개발자가 고의로 예외를 발생 시킬 수 있는 키워드입니다.좀더 직관적으로 설명하면 try구문내에서 throw를 통해 예외를 던지고(무조건 발생),이렇게 생성된 예외를 catch 구문에서 잡는다고 이해하셔도 됩니다.

위와 같은 방식으로 사용자 정의 예외를 발생시키는 경우는 빈번합니다.예를 들어 유저가 로그인을 할 경우,해당 유저에 대한 정보가 없을 경우 UserNotFound라는 예외를 직접 만들어서 처리가능합니다.위와 같은 방식은 한번만 사용자 지정 예외를 발생시키는 경우고 보통 이런 방식보다는 예외 클래스를 직접 작성하는 경우가 대다수입니다.

메서드에 예외 선언하기

예외를 처리하는 방법에는 지금까지 배운 try-catch를 사용하는 법과 예외를 메서드에 선언하는 방법이 있습니다.메서드에 예외를 선언하려면 메서드의 선언부에 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 됩니다.그리고 이러한 예외가 여러개 발생할 수 있을 경우 쉼표로 구분하여 여러개 적어주면 됩니다.

만약 아래와 같이 모든 예외의 조상인 Exception 클래스를 메서드에 선언하면,이 메서드는 모든 종류의 예외가 발생할 수도 있는 것입니다.

	void method() throws Exception1,Exception2{
		//...
	}

만일 위와 같은 구조에 예외의 최고 조상인 Exception 클래스를 메서드에 예외 선언해주면 이 메서드는 모든 종류의 예외가 발생할 가능성이 있는 메서드입니다.결론적으로 메서드에 선언되는 예외는 본인뿐만 아니라 상속관계를 통해 그 자손타입까지 예외가 발생할 수 있음을 고려해서 메서드에 예외를 선언해야합니다.

이제 예외를 메서드에 throw를 통해 선언하는 것과 실제로 예외처리를 하는것 간의 차이를 보여드리겠습니다.아래의 코드는 throw를 통해 예외가 발생할 수 있음을 각 메서드가 알려주고 있습니다.하지만 결정적으로 예외 처리,즉 try-catch를 통해 처리해주지 않았기에 예외가 발생하는 결과를 확인 할 수 있습니다.각각의 메서드가 자신을 호출할 다음 메서드에게 예외처리를 떠넘기는 형태라 보시면 됩니다.

public class Exception12 {
    public static void main(String[] args) throws Exception {
        method1();
    }

    static void method1() throws Exception {
        method2();
    }
    static void method2() throws Exception {
        throw new Exception();
    }
}

이번에는 메서드가 예외 처리를 하는 코드를 보겠습니다.

public class Exception13 {
    public static void main(String[] args) {
        method1();
    }
    static void method1() {
        try {
            throw new Exception();
        } catch (Exception e) {
            System.out.println("Exception Handling Complete Here!");
            e.printStackTrace();
        }
    }
}

예외는 method1()이라는 메서드에서 발생했고, 해당 메서드내에서 예의처리를 해주고 있는 것을 확인할 수 있습니다.위와 같이 예외가 발생한 메서드에서 바로 예외처리를 해줄수도 있습니다.반면에 예외가 발생한 method1()을 호출한 메서드에서 예외처리를 해주는 방식도 있습니다.즉,main 메서드에서 try-catch를 통해 예외처리를 진행해주면 됩니다.이와 같은 방식은 아래의 코드를 참고하면 됩니다.

public class ExceptionEx14 {
    public static void main(String[] args) {
        try {
            method1();
        } catch (Exception e) {
            System.out.println("main method handling Exception");
            e.printStackTrace();
        }
    }
    static void method1() throws Exception {
        throw new Exception();
    }
}

finally 블럭

간단하게 finally 블럭은 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시키는 기능을 수행합니다.try-catch문의 끝에 선택적으로 덧붙여 사용가능합니다.

다음 예제를 통해 사용해봅시다.

public class FinallyTest {
    public static void main(String[] args) {
        try {
            startInstall();
            copyFiles();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            deleteTempFiles();
        }
    }
    private static void deleteTempFiles() {
    }

    private static void copyFiles() {
    }

    private static void startInstall() {
    }
} 

deleteTempFiles()라는 메서드는 try구문과 catch구문 모두에서 호출하는 메서드인 경우 각각의 블럭에서 호출을 하기 보다는 finally블럭에서 호출을 하게되면 더욱 간결하게 코드를 짤 수 있습니다.

사용자 정의 예외 만들기

기존의 정의된 예외 클래스 외에 필요에 따라 개발자가 직접 예외 클래스를 정의하여 사용할수 있습니다.보통은 Exception 또는 RuntimeException클래스로부터 상속받아 클래스를 만들지만, 필요에 따라서 알맞은 예외 클래스를 상속하면 됩니다.

아래와 같이 예외 클래스를 만들면 됩니다.

public class MyException extends Exception {
    private final int ERR_CODE;

    MyException(String message, int ERR_CODE) {
        super(message);
        this.ERR_CODE = ERR_CODE;
    }

    MyException(String msg) {
        this(msg, 100);
    }

    public int getERR_CODE() {
        return ERR_CODE;
    }
}

Ref : 자바의 정석 - 남궁성