자바 예외처리

계리·2023년 1월 23일
0
post-thumbnail
post-custom-banner

프로그램 오류

프로그램이 실행 중 어떤 원인에 의해 오작동을 하거나 비정상적으로 종료되는 경우가 있다. 이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다.

발생 지점에 따라 컴파일 에러, 런타임 에러, 논리적 에러가 있다.

  • 컴파일 에러 : 컴파일 시에 발생하는 에러
  • 런타임 에러 : 실행 시에 발생하는 에러
  • 논리적 에러 : 실행은 되지만 의도와 다르게 동작하는 것

자바에서는 실행 시 발생할 수 있는 프로그램 오류를 에러와 예외 두 가지로 구분하였다.

  • 에러 : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
  • 예외 : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류

예외 클래스의 계층 구조

위 그림 처럼 예외 클래스들을 두 그룹으로 나눠볼 수 있다.

  • Exception클래스와 그 자손들(RuntimeException과 자손들 제외)
  • RuntimeException클래스와 그 자손들

Exception클래스들 : 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외
RuntimeException클래스들 : 프로그래머의 실수로 발생하는 예외


예외처리하기 try-catch문

프로그램 실행도중에 발생하는 에러는 어쩔 수 없지만 예외는 프로그래머가 이에 대한 처리를 미리 해주어야 한다.

예외처리란

  • 정의 : 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것
  • 목적 : 프로그램의 비정상 종료를 막고 정상적인 실행상태를 유지하는 것

발생한 예외를 처리하지 못하면 프로그램은 비정상적으로 종료되며 처리되지 못한 예외(uncaught exception)는 JVM의 예외처리기(UncaughtExceptionHandler)가 받아서 예외의 원인을 화면에 출력한다,


예제1

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

        for(int i = 0; i < 10; i++){
            result = number / (int)(Math.random() * 10);    // 7번째 라인
            System.out.println(result);
        }
    }   // main의 끝
}

실행결과
50
33
50
14
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at JOJ.CH08.CH08_02.ExceptionEx2.main(ExceptionEx2.java:7)

위 예제에서 변수 number에 저장되어 있는 값 100을 0~9 사이 임의의 값을 정수로 나눈 결과를 출력하는 일을 10번 반복한다.

random()을 사용했기 때문에 매번 결과가 다르겠지만 왠만하면 10번을 모두 반복하기 전에 에러가 발생할 것이다.

에러 메세지를 확인해보면 ArithmeticException이 7번째 라인에서 발생했는데 해당 에러는 산술연산과정에서 오류가 발생했을 때 발생하는 예외이며 정수는 0으로 나누는 것을 금지되어있기 때문에 발생했다.


예제2

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

        for(int i = 0; i < 10; i++){
            try{
                result = number / (int)(Math.random() * 10);
                System.out.println(result);
            } catch (ArithmeticException e){
                System.out.println("0");       // ArithmeticException이 발생하면 실행되는 코드
            }   // try-catch의 끝
        }   // for문의 끝
    }
}

실행결과
50
100
14
100
16
14
0 ← ArithmeticException이 발생하여 0이 출력되었다.
33
16
50

위 예제는 try-catch문을 추가한 것이다. 실행결과를 보면 ArithmeticException이 발생할 경우 0이 출력되도록 구성하였다.

try에서 이상없이 작동하면 반복문을 정상적으로 작동하다가 오류가 발생하면 catch문으로 넘어가서 해당 로직을 수행하게 된다.

이렇게 예외처리를 해주면 예외가 발생하더라도 정상적으로 프로그램을 종료할 수 있게 된다.


try-catch문에서의 흐름

  • try블럭 내에서 예외가 발생한 경우
    1. 발생한 예외와 일치하는 catch블럭이 있는지 확인한다.
    2. 일치하는 catch블럭을 찾게 되면 그 catch블럭 내의 문장들을 수행하고 전체 try-catch문을 빠져나가서 그 다음 문장을 계속해서 수행한다. 만일 일치하는 catch블럭을 찾지 못하면 예외는 처리되지 못한다.
  • try블럭 내에서 예외가 발생하지 않은 경우
    1. catch블럭을 거치지 않고 try-catch문을 빠져나가서 수행을 계속한다.

예제1

public class ExceptionEx4 {
    public static void main(String[] args){
        System.out.println(1);
        System.out.println(2);
        try{
            System.out.println(3);
            System.out.println(4);
        } catch (Exception e){
            System.out.println(5);  // 실행되지 않는다.
        }   // try-catch의 끝
        System.out.println(6);
    }   // main메서드의 끝
}

실행결과
1
2
3
4
6

try블럭 내에서 예외가 발생하지 않는 경우 catch블럭을 거치지 않고 수행하는 것을 확인할 수 있다.


예제2

public class ExceptionEx5 {
    public static void main(String[] args){
        System.out.println(1);
        System.out.println(2);
        try{
            System.out.println(3);
            System.out.println(0/0);
            System.out.println(4);  // 실행되지 않는다.
        } catch (ArithmeticException ae){
            System.out.println(5);
        }   // try-catch의 끝
        System.out.println(6);
    }   // main메서드의 끝
}

실행결과
1
2
3
5
6

try블럭 내에서 예외가 발생할 경우 발생한 라인에서 catch블럭으로 넘어가 수행하는 것을 확인할 수 있다.


예외의 발생과 catch블럭

try블럭에서 예외가 발생하게 되면 첫 번째 catch블럭부터 차례대로 수행되는데 catch블럭 괄호내에 선언된 참조변수의 종류와 생성된 예외클래스의 인스턴스에 instanceof연산자를 이용해서 검사하고 true인 catch블럭이 만날 때가지 계속 검사한다.

true인 catch블럭을 만나게 되면 블럭 내에 문장들을 수행하고 try-catch문을 빠져나가고 예외는 처리되지만 true인 catch문을 만나지 못하면 예외는 처리되지 않는다.

모든 예외 클래스는 Exception클래스의 자손이므로 catch블럭의 괄호에 Exception클래스 타입의 참조변수를 선언해 놓으면 어떤 종류의 예외가 발생하더라도 이 catch블럭에 의해서 처리된다.


예제1

public class ExceptionEx6 {
    public static void main(String[] args){
        System.out.println(1);
        System.out.println(2);
        try{
            System.out.println(3);
            System.out.println(0/0);
            System.out.println(4);  // 실행되지 않는다.
        } catch (Exception e){
            System.out.println(5);
        }   // try-catch의 끝
        System.out.println(6);
    }   // main메서드의 끝
}

실행결과
1
2
3
5
6

예외의 발생과 catch블럭 예제2 코드에서 catch블럭의 괄호에 ArithmeticException타입의 참조변수 대신 Exception클래스의 참조변수로 변경한 것인데 결과는 같다.

Exception클래스는 모든 예외 클래스의 최상위 클래스이기 때문에 ArithmeticException이 발생해도 catch블럭의 괄호에 Exception클래스 참조변수로 선언되어있어 해당 catch블럭에서 true가 발생하여 문장들을 수행하게 된다.


예제2

public class ExceptionEx7 {
    public static void main(String[] args){
        System.out.println(1);
        System.out.println(2);
        try{
            System.out.println(3);
            System.out.println(0/0);
            System.out.println(4);  // 실행되지 않는다.
        } catch (ArithmeticException ae){
            if(ae instanceof ArithmeticException)
                System.out.println("true");
            System.out.println("ArithmeticException");
        } catch (Exception e){
            System.out.println("Exception");
        }   // try-catch의 끝
        System.out.println(6);
    }   // main메서드의 끝
}

실행결과
1
2
3
true
ArithmeticException
6

위 예제에서는 ArithmeticException클래스 타입의 참조변수를 선언한 catch블럭과 Exception클래스 타입의 참조변수를 선언한 catch블럭이 있다.

실행결과를 확인해보면 차례대로 try-catch블럭을 수행 중 try블럭에서 예외가 발생하여 발생한 예외 클래스와 catch문에 선언된 예외 클래스가 일치한 것을 확인 후 해당 catch블럭 문장들을 수행 후 try-catch블럭을 끝낸 것을 확인할 수 있다.

위 2개의 예제를 확인해봤을 때 좀 더 정확한 예외처리를 하기 위해서는 Exception클래스를 첫 catch블럭에 선언하는 것은 지양 해야하는 것으로 생각이 된다.

첫 catch블럭에 Exception클래스를 선언하게 되면 어떤 예외가 발생하더라도 Exception클래스가 선언된 catch블럭에서 수행되기 때문에 예외처리 구분이 힘들어질 것으로 보여진다.


printStackTrace()와 getMessage()

예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨져 있는데 printStackTrace()와 getMessage()로 확인할 수 있다.


printStackTrace()
예외발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와 예외 메세지를 화면에 출력한다.


getMessage()
발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.


예제1

    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try{
            System.out.println(3);
            System.out.println(0/0);    // 예외발생!!
            System.out.println(4);      // 실행되지 않는다.
        } catch (ArithmeticException ae){
            ae.printStackTrace();       // 참조변수 ae를 통해 생성된 ArithmeticException인스턴스에 접근할 수 있다.
            System.out.println("예외 메시지 : " + ae.getMessage());
        }   // try-catch 끝
        System.out.println(6);
        //  main메서드의 끝
    }
 
결과
1
2
3
java.lang.ArithmeticException: / by zero
	at JOJ.CH08.CH08_08.ExceptionEX8.main(ExceptionEX8.java:9)
예외 메시지 : / by zero
6

메서드에 예외 선언하기

try-catch문을 사용하는 것 외에 메서드에 선언하는 방법이 있다. 선언하는 방법은 아래와 같다.

void method() throws Exception1, Exception2, ... ExceptionN {
	// 메서드의 내용
{

만일 아래와 같이 모든 예외의 최고조상인 Exception클래스를 메서드에 선언하면 모든 종류의 예외가 발생할 가능성이 생긴다.

void method() throws Exception{
	// 메서드의 내용
}

위와 같이 예외를 선언하면 자손타입의 예외까지도 발생할 수 있기 때문에 주의해야 한다.


메서드에 예외를 선언할 때 throws를 명시하는 것은 예외를 처리하는 것이 아니라 자신(예외가 발생할 가능성이 있는 메서드)을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다.

public class ExceptionEx12 {
    public static void main(String[] args) throws Exception{
        method1();  //  같은 클래스내의 static멤버이므로 객체생성없이 직접 호출가능
    }

    static void method1() throws Exception {
        method2();  // method1의 끝
    }

    static void method2() throws Exception {
        throw new Exception();
        //  method2의 끝
    }
}

결과
Exception in thread "main" java.lang.Exception
	at JOJ.CH08.CH08_12.ExceptionEx12.method2(ExceptionEx12.java:13)
	at JOJ.CH08.CH08_12.ExceptionEx12.method1(ExceptionEx12.java:9)
	at JOJ.CH08.CH08_12.ExceptionEx12.main(ExceptionEx12.java:5)

위와 같이 프로그램 실행도중 java.lang.Exception이 발생하여 비정상적으로 종료했다는 것과 예외가 발생했을 때 호출스택(call stack)의 내용을 알 수 있다.

  • 예외가 발생했을 때 모두 3개의 메서드(main, method1, method2)가 호출스택에 있엇으며
  • 예외가 발생한 곳은 제일 윗줄에 있는 method2()라는 것과
  • main메서드가 method1()을, 그리고 method1()은 method2()를 호출했다는 것을 알 수 있다.

finally블럭

finally블럭은 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용된다.
try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며 try-catch-finally의 순서로 구성된다.

	try{
    	// 예외가 발생할 가능성이 있는 문장들을 넣는다.
    } catch (Exception e1) {
    	// 예외처리를 위한 문장을 적는다.
    } finally {
    	// 예외의 발생여부에 관게없이 수행되어야하는 문장들을 넣는다.
        // finally블럭은 try-catch문의 맨 마지막에 위치해야한다.
    }

예외가 발생한 경우에는 try-catch-finally순으로 실행되고 예외가 발생하지 않은 경우 try-finally의 순으로 실행된다.

public class FinallyTest {
    public static void main(String[] args) {
        try{
            startInstall();     // 프로그램 설치에 필요한 준비를 한다.
            copyFiles();        // 파일들을 복사한다.
            deleteTempFiles();  // 프로그램 설치에 사용된 임시파일들을 삭제한다.
        } catch (Exception e){
            e.printStackTrace();
            deleteTempFiles();  // 프로그램 설치에 사용된 임시파일들을 삭제한다.
        }   // try-catch의 끝
    }   // main의 끝
    
    static void startInstall(){
        /*  프로그램 설치에 필요한 준비를 하는 코드들이 작성되어 있다고 가정.   */
    }
    static void copyFiles(){
        /*  파일들을 복사하는 코드들이 작성되어 있다고 가정.   */
    }
    static void deleteTempFiles(){
        /*  임시파일들을 삭제하는 코드들이 작성되어 있다고 가정.   */
    }
}

위의 코드를 확인해보면 프로그램 설치를 한 후 파일들을 복사하고 복사가 완료되면 임시파일들을 삭제하도록 try문에 작성이 되어있고 프로그램 설치 예외가 발생하여도 catch문에서 임시파일들을 삭제할 수 있도록 작성이 되어있다. 위와 같이 꼭 예외처리에서 꼭 실행이 되어야 하는 부분이 있다면 아래와 같이 finally블럭을 이용하면 된다.

public class FinallyTest2 {
    public static void main(String[] args) {
        try{
            startInstall();     // 프로그램 설치에 필요한 준비를 한다.
            copyFiles();        // 파일들을 복사한다.
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            deleteTempFiles();  // 프로그램 설치에 사용된 임시파일들을 삭제한다.
        }   // try-catch의 끝
    }   // main의 끝

    static void startInstall(){
        /*  프로그램 설치에 필요한 준비를 하는 코드들이 작성되어 있다고 가정.   */
    }
    static void copyFiles(){
        /*  파일들을 복사하는 코드들이 작성되어 있다고 가정.   */
    }
    static void deleteTempFiles(){
        /*  임시파일들을 삭제하는 코드들이 작성되어 있다고 가정.   */
    }
}

public class FinallyTest3 {
    public static void main(String[] args) {
        //  method1()은 static메서드이므로 인스턴스 생성없이 직접 호출이 가능하다.
        FinallyTest3.method1();
        System.out.println("method1()의 수행을 마치고 main메서드로 돌아왔습니다.");
    }   // main메서드의 끝

    static void method1(){
        try{
            System.out.println("method1()이 호출되었습니다.");
            return;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("method1()의 finally블럭이 실행되었습니다.");
        }
    }   // method1메서드의 끝
}
결과
method1()이 호출되었습니다.
method1()의 finally블럭이 실행되었습니다.
method1()의 수행을 마치고 main메서드로 돌아왔습니다.

위의 결과를 확인하면 알 수 있듯이 try블럭에서 return문이 실행되는 경우에도 finally블럭의 문장들이 먼저 실행된 후에 현재 실행 중인 메서드를 종료한다.


예외 되던지기

한 메서드에서 발생할 수 있는 예외가 여럿인 경우 몇 개는 try-catch문을 통해서 메서드 내에서 자체적으로 처리하고 그 나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록 함으로써 양쪽에서 나눠서 처리되도록 할 수 있다.

public class ExceptionEx17 {
    public static void main(String[] args) {
        try{
            method1();
        } catch (Exception e){
            System.out.println("main메서드에서 예외가 처리되었습니다.");
        }
    }   // main메서드의 끝

    static void method1() throws Exception {
        try{
            throw new Exception();
        } catch (Exception e){
            System.out.println("method1 메서드에서 예외가 처리되었습니다.");
            throw e;        // 다시 예외를 발생시킨다.
        }
    }   // method1 메서드의 끝
}
결과
method1 메서드에서 예외가 처리되었습니다.
main메서드에서 예외가 처리되었습니다.

※ 참고 문헌

남궁성, 『Java의 정석 3nd Edition』, 도우출판(2016) 책으로 공부하고 정리한 내용 입니다.

profile
gyery
post-custom-banner

0개의 댓글