7) 예외1 - 예외 처리

dev-mage·2022년 10월 27일
0

Hello Java World!

목록 보기
20/32
post-thumbnail

Java의 예외와 예외 처리를 위한 try-catch문

프로그램 오류

프로그램이 실행 중 어떤 원인에 의해 오작동하거나 비정상적으로 종료되는 결과를 초래하는 원인을 에러 또는 오류라고 한다. 에러는 발생 시점에 따라 컴파일 에러(compile-time error)런타임 에러(runtime error)로 나눌 수 있고 이 외에 논리적 에러(logical error)가 있다.

  • 컴파일 에러: 컴파일 시 발생하는 에러
  • 런타임 에러: 프로그램 실행 시 발생하는 에러
  • 논리적 에러: 컴파일 및 실행은 되지만, 의도와 다르게 동작하는 경우

소스코드를 컴파일하면 컴파일러가 소스코드(*.java)에 대해 오타나 잘못된 구문, 자료형 체크 등의 기본적인 검사를 수행하여 오류가 있는지 알려 준다. 컴파일러가 알려 준 에러를 모두 수정해서 컴파일에 성공하면 클래스 파일(*.class)이 생성되고, 생성된 클래스 파일을 실행할 수 있다.

하지만 컴파일을 마쳤다고 해서 프로그램이 완벽하게 실행되는 것은 아니다. 컴파일러가 실행 도중 발생할 수 있는 잠재적인 오류까지 검사할 수 없기 때문에 컴파일은 잘 되었어도 실행 중에 잘못된 결과를 얻거나 프로그램이 비정상적으로 종료될 수 있다. 자바에서는 이렇게 실행 시 발생할 수 있는 프로그램 오류를 에러(error)와 예외(exception)로 구분하고 있다.

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

예외 클래스

자바에서는 실행 시 발생할 수 있는 오류를 클래스로 정의하였다. 모든 클래스의 조상은 Object 클래스이므로 Exception과 Error 클래스 역시 Object 클래스의 자식이다. 모든 예외의 최고 부모는 Exception 클래스이며 예외 클래스는 크게 Exception 클래스와 하위 클래스들 그리고 RuntimeException 클래스와 하위 클래스들로 나눌 수 있다. 에러와 예외는 모두 프로그램 실행 시(runtime) 발생하는 오류이다.

  • Exception 클래스 주로 외부의 영향으로 발생할 수 있는 예외이다. 예를 들면 존재하지 않는 파일의 이름을 입력했다든지(FileNotFoundException), 입력한 데이터 형식이 잘못된 경우(DataFormatException)와 같이 사용자의 실수와 같은 외적인 요인에 의해 발생한다.
  • RuntimeException 클래스 주로 프로그래머의 실수에 의해서 발생될 수 있는 예외이다. 예를 들면 배열의 범위를 벗어난다든가(ArrayIndexOutOfBoundsException), 값이 null인 참조 변수를 호출하려 했다든가(NullPointerException), 정수를 0으로 나누려고 하는 경우(ArithmeticException)에 발생한다.

예외 처리: try-catch문

프로그램의 실행 도중 발생하는 에러는 어쩔 수 없지만 예외는 프로그래머가 이에 대한 처리를 미리 해주어야 한다. 예외 처리(exception handling)란 프로그램 실행 시 발생할 수 있는 예기치 못한 예외의 발생에 대비하여 코드를 작성하는 것이며, 예외 처리의 목적은 예외 발생으로 인한 프로그램의 갑작스런 비정상 종료를 막고, 정상적인 실행상태를 유지할 수 있도록 하는 것이다. 발생한 예외를 처리하지 못하면 프로그램은 비정상적으로 종료되며, 처리되지 못한 예외(uncaught exception)는 JVM의 예외처리기(UncaughtExceptionHandler)가 받아서 예외의 원인을 화면에 출력한다.

예외를 처리하기 위해서는 try-catch문을 사용한다.

try {
		// 예외가 발생할 가능성이 있는 코드
		// if문과 달리 try나 catch 블럭 내의 문장이 한 줄 뿐이어도 괄호{}를 생략할 수 없음.
} catch(Exception1 e1) {
		// Exception1이 발생했을 경우, 이를 처리하기 위한 코드
} catch(Exception2 e2) {
		// Exception2이 발생했을 경우, 이를 처리하기 위한 코드
} catch(ExceptionN eN) {
		// ExceptionN이 발생했을 경우, 이를 처리하기 위한 코드
}

하나의 try 블럭 다음에는 여러 예외를 처리할 수 있도록 하나 이상의 catch 블럭이 올 수 있으며, 이 중 발생한 예외와 일치하는 단 한 개의 catch 블럭만 수행된다. 발생한 예외와 일치하는 catch 블럭이 없으면 예외는 처리되지 않는다.

try-catch문의 흐름

예외가 발생했을 때와 발생하지 않았을 때의 try-catch문은 다르게 실행된다.

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

다음 예제에서는 예외가 발생하지 않았으므로 catch 블럭이 실행되지 않았다.

public class ExceptionFlow {
    public static void main(String[] args) {
        System.out.println(1);
        try {
            System.out.println(2);
        } catch (Exception e) {
            System.out.println(3); // 실행되지 않음
        }
        System.out.println(4);
        
        // 실행 결과: 1 2 4
    }
}

위 예제를 변경해서 0으로 나누어 고의로 예외를 발생시켰다.

public class ExceptionFlow {
    public static void main(String[] args) {
        System.out.println(1);
        try {
            System.out.println(0/0);
            System.out.println(2);  // 실행되지 않음
        } catch (ArithmeticException ae) {
            System.out.println(3);
        }
        System.out.println(4);

        // 실행 결과: 1 3 4
    }
}

예외가 발생했기 때문에 try 블럭을 바로 벗어나 ‘2’는 출력되지 않는다.

catch 블럭

catch 블럭은 괄호()와 블럭{}으로 나누어져 있는데 괄호 내에는 처리하고자 하는 예외와 같은 타입의 참조 변수 하나를 선언해야 한다. 예외가 발생하면 발생한 예외에 해당하는 클래스의 인스턴스가 만들어 진다. 첫 번째 catch 블럭부터 차례로 내려가면서 catch 블럭에 선언된 참조 변수와 생성된 예외 클래스의 인스턴스를 instanceof 연산자로 검사하며 검사 결과가 true인 catch 블럭을 만날 때까지 계속된다. 검사 결과가 true인 catch 블럭을 찾게 되면 블럭에 있는 코드를 모두 수행한 후 try-catch문을 빠져나가고 예외는 처리되지만 찾지 못하면 예외는 처리되지 않는다.

모든 예외 클래스는 Exception 클래스의 자식이므로 catch 블럭에 Exception 클래스 타입의 참조 변수를 선언하면 어떤 종류의 예외가 발생하더라도 처리할 수 있다. 따라서 작은 범위의 예외부터 선언하여 마지막 catch 블럭에 Exception 타입의 참조 변수를 선언해야 한다.

아래 코드는 Exception 'java.lang.ArithmeticException' has already been caught이라는 컴파일 에러가 발생한다. Exception 클래스에 의해 예외가 이미 처리되었기 때문이다.

public class ExceptionFlow {
    public static void main(String[] args) {
        System.out.println(1);
        try {
            System.out.println(0/0);
            System.out.println(2);
        } catch (Exception e) {
            System.out.println(3);
        } catch (ArithmeticException e) { // 에러!!
            System.out.println(4);
        }
        System.out.println(5);
    }
}

다음과 같이 작은 범위의 예외부터 선언하면 에러가 해결된다.

public class ExceptionFlow {
    public static void main(String[] args) {
        System.out.println(1);
        try {
            System.out.println(0/0);
            System.out.println(2);
        } catch (ArithmeticException ae) { // 에러 해결
            System.out.println(3);
        } catch (Exception e) {
            System.out.println(4);
        }
        System.out.println(5);
        
        // 실행 결과: 1 3 5
    }
}

다중 catch 블럭

JDK 7부터 여러 catch문을 |기호를 이용해서 하나의 catch 블럭으로 합칠 수 있게 되었으며 이를 다중 catch블럭이라고 한다. |기호로 연결할 수 있는 예외 클래스의 개수에는 제한이 없다.

try {
		...
} catch(Exception1 e1) {
		...
} catch(Exception2 e2) {
		...
}

///////////////////////////////////////////////////////////////

try {
		...
} catch(Exception1 | Exception2 e) {
		...
}

만일 다중 catch 블럭의 예외 클래스가 부모와 자식 관계라면 컴파일 에러가 발생한다. 아래 예제는 Types in multi-catch must be disjoint: 'java.lang.ArithmeticException' is a subclass of 'java.lang.Exception’라는 컴파일 에러가 발생한다.

public class ExceptionFlow {
    public static void main(String[] args) {
        System.out.println(1);
        try {
            System.out.println(0/0);
            System.out.println(2);
        } catch (Exception | ArithmeticException ae) { // 에러!!
            System.out.println(3);
        }
        System.out.println(5);
    }
}

두 예외 클래스가 부모 자식 관계라면 부모 클래스만 쓰는 것과 같기 때문에 불필요한 코드는 제거하라는 뜻에서 발생하는 에러이다.

다중 catch 블럭은 하나의 catch 블럭으로 여러 예외를 처리하는 것이므로 실제로 어떤 예외가 발생한 것인지 알 수 없다. 그러므로 다중 catch 블럭에 선언된 예외 클래스의 공통 분모인 부모 클래스의 멤버만 사용할 수 있다.

try {
		... 
} catch (TypeNotPresentException e) {
    e.typeName();
}

///////////////////////////////////////////////////////////////

try {
		... 
} catch (ClassCastException | TypeNotPresentException e) {
    // e.typeName(); -> 에러!!!
		e.getMessage(); // -> 공통 부모 클래스인 Throwable의 메서드
}

finally 블럭

finally 블럭은 예외의 발생 여부에 상관없이 실행되어야 할 코드를 포함하기위해 사용된다. try 블럭과 catch 블럭에 같은 코드를 작성하지 않고 finally 블럭에 작성하면 된다. try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며 try-catch-finally의 순서로 작성한다. 예외가 발생한 경우 try → catch → finally 순으로 실행되고 예외가 발생하지 않은 경우 try → finally 순으로 실행된다. try 블럭에 return문이 있어서 try 블럭을 벗어나도 finally 블럭은 실행된다.


References

  • 자바의 정석 CHAPTER 8

0개의 댓글