개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

기초개념 › Java

예외

예외는 무엇이고, try-catch는 무엇을 해 주는가

예외는 프로그램을 실행하는 중에 계속할 수 없는 일이 생겼을 때 자바가 던지는 객체입니다. 예를 들어 정수를 0으로 나누면 자바가 ArithmeticException이라는 예외를 던지고, 던지는 순간 하던 일을 그 자리에서 멈춥니다. 아무도 잡지 않으면 프로그램은 거기서 끝나고 그 뒤의 줄은 실행되지 않습니다. try { }는 문제가 날 수 있는 코드를 감싸는 것이고, 뒤에 붙인 catch (예외의 종류 e) { }는 그 종류의 예외가 났을 때 실행이 건너와 처리하는 곳입니다. 처리가 끝나면 try-catch 아래의 줄을 계속 실행합니다. 이 실습은 try-catch를 지우고, b의 값을 바꾸고, catch가 잡는 종류를 바꿔 가며 같은 코드를 컴파일하고 실행해, 무엇이 출력되고 프로그램이 어떻게 끝나는지를 봅니다.

실행으로 대조 · JDK 21에서 여덟 가지 조합을 javax.tools로 컴파일하고 자식 java 프로세스로 실행해, 표준 출력·표준 오류·종료 코드를 대조

이 실습에 나오는 개념

예외(Exception)와 Error
둘 다 프로그램 실행 중의 비정상 상황을 나타내는 Throwable의 하위 타입이다. Exception은 애플리케이션이 잡아서 처리할 수 있는 상황(잘못된 입력, 파일 없음 등)이고, Error는 메모리 부족이나 스택 오버플로처럼 JVM 수준의 심각한 문제라 애플리케이션이 복구를 시도하지 않는 것이 일반적이다.
try-catch
try-catch는 예외가 났을 때 실행이 건너가 처리할 곳을 정하는 문장이다. 문제가 날 수 있는 코드를 try { }로 감싸고, 뒤에 catch (예외의 종류 e) { }를 붙인다. try 안에서 예외가 던져지면 실행이 거기서 멈추고, 던져진 예외의 종류가 catch에 적은 종류이거나 그 하위 종류일 때 catch 안으로 건너가 실행한 뒤, try-catch 아래의 줄로 이어 간다. catch가 여러 개면 위에서부터 맞는 첫 번째 것이 쓰이고, 맞는 catch가 없으면 예외는 try-catch 밖으로 계속 전달되어 아무도 잡지 않으면 그 스레드가 끝난다. 예외가 나지 않으면 catch 안은 실행되지 않는다.
예외를 던진다(throw)
예외를 던진다는 것은 예외 객체를 만들어 지금 하던 일을 멈추고, 그것을 잡는 곳으로 실행을 옮기는 것이다. 정수를 0으로 나누는 것처럼 자바의 규칙을 어기는 일이 생기면 자바가 스스로 예외를 던지고, throw new IllegalStateException()처럼 throw 문으로 코드에서 직접 던질 수도 있다. 던져지면 던진 곳으로 되돌아가 이어서 실행하지 않는다. 호출한 쪽으로 차례로 올라가며 이 예외를 잡을 수 있는 try-catch를 찾고, 끝까지 없으면 그 스레드가 끝난다.
NullPointerException
null인 참조로 메서드를 부르거나 필드를 읽으려고 할 때 발생하는 예외다. null은 가리키는 객체가 없다는 뜻이므로, 없는 객체에게 일을 시킬 수 없어서 예외가 난다. null인지 아닌지는 실행 중의 값에 달려 있어서 컴파일러가 미리 잡아 주지 못하고, 실행해 봐야 드러난다(Unchecked 예외라 처리를 강제하지도 않는다). null을 받지 않도록 만든 메서드(Optional.of, Objects.requireNonNull 등)에 null을 넘겨도 이 예외가 발생한다. 값이 없을 수 있는 곳에서는 먼저 null인지 검사하거나, Optional로 값이 없을 수 있음을 타입에 드러내어 줄인다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. java Main으로 실행합니다. main 메서드가 시작되고 int a = 10; 줄과 int b = 0; 줄이 차례로 실행되어, 변수 a에 담은 값은 10, 변수 b에 담은 값은 0입니다.
  2. try { 줄에 닿습니다. try 안의 줄은 평소처럼 위에서부터 차례로 실행됩니다. try는 "이 안에서 예외가 나면 아래의 catch로 넘기겠다"고 미리 알려 두는 것이고, 예외가 나지 않으면 try를 쓰지 않은 것과 같게 실행됩니다.
  3. System.out.println(a / b); 줄을 실행합니다. println이 출력하려면 먼저 괄호 안의 a / b를 계산해야 합니다. 10 / 0을 계산하려는 순간, 자바(JVM)가 정수를 0으로 나눌 수 없다는 것을 알고 ArithmeticException이라는 예외 객체를 만들어 던집니다(throw). 예외가 던져지면 자바는 그 자리에서 하던 일을 멈추므로, 출력할 값이 없는 println은 실행되지 않습니다.
  4. 자바가 이 예외를 잡는 곳이 있는지 찾습니다. 예외가 난 곳은 try 안이므로 이 try에 붙은 catch를 봅니다. catch (ArithmeticException e)는 ArithmeticException 종류를 잡겠다고 적혀 있고, 던져진 것도 ArithmeticException이므로 종류가 맞습니다.
  5. 실행이 catch 안으로 건너갑니다. 던져진 예외 객체는 변수 e에 담기고, catch 안의 System.out.println("0으로 나눌 수 없습니다"); 줄이 실행되어 그 글을 출력합니다.
  6. catch 안의 줄이 끝나면 예외는 처리된 것으로 칩니다. 프로그램은 멈추지 않고 try-catch 아래의 줄로 이어 가서, System.out.println("끝"); 줄이 "끝"을 출력합니다. main의 마지막 줄까지 실행되어 프로그램이 끝납니다.

실행 결과

코드

public class Main {
    public static void main(String[] args) {
        int a = 10;
        int b = 0;
        // try: 문제가 날 수 있는 코드를 감싼다
        try {
            System.out.println(a / b);
        // catch: 예외가 나면 여기로 건너온다
        } catch (ArithmeticException e) {
            System.out.println("0으로 나눌 수 없습니다");
        }
        System.out.println("끝");
    }
}

이 모형의 전제

컴파일러와 실행 환경은 JDK 21.0.12.1(javac 21)입니다. 여덟 가지 조합을 모두, 화면의 코드와 같은 소스(줄 단위로 같은 글)를 javax.tools로 컴파일해 모두 컴파일되는 것을 확인했고, 컴파일된 Main을 이 JDK의 java를 자식 프로세스로 띄워 실제로 실행해 표준 출력, 표준 오류, 종료 코드를 잡은 결과와 대조했습니다. 대조한 것은 컴파일 성공 여부, 표준 출력의 줄들(결과 표의 출력 열), 종료 코드와 표준 오류의 첫 줄(프로그램이 끝난 방식 열: 정상이면 종료 코드 0에 표준 오류가 비어 있었고, 멈추면 종료 코드 1에 첫 줄이 Exception in thread "main" java.lang.ArithmeticException: / by zero였으며 표준 오류에 Main.main이 있는 스택 추적이 이어졌습니다), catch 안의 코드가 실행됐는지(catch 안의 println이 출력한 글이 표준 출력에 있는지)입니다. 멈춘 조합은 모두 표준 출력이 비어 있어 "끝"이 출력되지 않았습니다. try-catch가 없는 조합은 catch가 잡는 종류 설정이 쓰이지 않아 두 조합의 결과가 같습니다. 문서는 Java Language Specification(Java SE 21)을 직접 열어 확인했습니다. 규칙을 어기면 자바 가상 머신이 예외로 알리고 프로그램이 정한 곳으로 제어가 옮겨진다는 것은 11장 서두, 예외가 던져지면 가장 가까이 둘러싼 try의 그 예외를 처리할 수 있는 catch로 제어가 옮겨지고 예외를 일으킨 코드는 다시 실행되지 않으며 처리할 catch가 없으면 그 스레드가 끝난다는 것은 11.3절, throw가 즉시 제어를 옮기고 맞는 try가 없으면 그 스레드가 끝난다는 것은 14.18절, catch의 매개변수는 하나이고 그 종류는 Throwable이거나 그 하위 클래스여야 한다는 것은 14.20절, 던져진 값의 종류가 catch에 적은 종류에 대입할 수 있으면 가장 왼쪽의 맞는 catch가 선택되어 그 블록이 실행되고 그 블록이 정상으로 끝나면 try 문도 정상으로 끝나며 맞는 catch가 없으면 try 문이 같은 예외로 끝난다는 것은 14.20.1절, 정수 나눗셈에서 나누는 수가 0이면 ArithmeticException이 던져진다는 것은 15.17.2절을 따랐습니다. 14.20.1절의 맞는 종류에는 적은 종류의 하위 종류도 들어가지만, 이 실습의 catch는 ArithmeticException과 NullPointerException뿐이고 둘은 서로 상하 관계가 아니어서 하위 종류를 잡는 경우는 다루지 않았습니다. 표준 오류에 나온 글과 종료 코드 1은 명세가 정한 것이 아니라 이 java에서 관찰한 것입니다. 화면의 코드는 java Main으로 실행하는 모양 그대로이고 패키지는 없습니다. 그림의 10 / 0과 10 / 2는 a와 b에 담은 값을 풀어 쓴 것입니다. 예외에는 이 밖에도 catch를 여러 개 쓰는 것, finally, throw로 직접 던지기, 호출한 메서드를 거슬러 올라가며 잡는 곳을 찾는 것, 체크 예외가 있지만 이 실습은 다루지 않았습니다. 이것들은 "세부 동작 실습: 예외와 finally"에서 다룹니다.

앞 실습: 인터페이스 · 다음 실습: 제네릭

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요