JAVA

자바 다중 catch와 예외 던지기 — 여러 오류를 골라서 처리하기(THROW·THROWS)

PANCO_IT
#자바#JAVA#예외처리#다중catch#throw#throws

다중 catch는 좁은 예외부터 넓은 예외 순으로 배치한다는 그림

예외는 위에서부터 내려오며 자기에게 맞는 catch에서 걸립니다. 그래서 순서가 중요합니다.

1편에서 안전망을 쳤습니다. try로 감싸고 catch로 붙잡으면 프로그램이 죽지 않는다는 것까지 배웠죠. 그런데 실제 코드는 한 종류의 오류만 내지 않습니다. 같은 블록 안에서 0으로 나누기도 하고, 없는 배열 칸을 읽기도 합니다. 오류마다 대응이 달라야 할 때 어떻게 하는지, 그리고 예외를 내가 직접 만들어 던지는 방법까지가 오늘의 주제입니다.

catch는 여러 개 달 수 있습니다

방법은 간단합니다. try 하나에 catch를 여러 개 이어 붙이면 됩니다. 예외가 나면 위에서부터 훑어 내려가다가 타입이 맞는 첫 번째 catch에서 걸리고, 나머지는 건너뜁니다.

try {
    int[] nums = {10, 20, 30};
    System.out.println(nums[0] / 0);      // 여기서 ArithmeticException
} catch (ArithmeticException e) {
    System.out.println("0으로는 나눌 수 없습니다.");
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("배열 범위를 벗어났습니다.");
}

핵심은 오류 종류별로 다른 메시지, 다른 대응을 줄 수 있다는 것입니다. 계산 오류면 기본값을 쓰고, 범위 오류면 인덱스를 다시 받는 식으로요. 1편처럼 catch 하나로 뭉뚱그리면 "뭔가 잘못됐다"까지만 알 수 있지만, 나눠 잡으면 "무엇이 잘못됐는지"까지 코드가 구분합니다.

참고로 대응이 똑같은 예외들은 세로 막대 기호로 묶어서 한 번에 잡을 수도 있습니다.

} catch (ArithmeticException | ArrayIndexOutOfBoundsException e) {
    System.out.println("입력값을 확인해 주세요: " + e.getMessage());
}

단골 함정 — 순서를 거꾸로 쓰면 컴파일이 안 됩니다

여기서 입문자가 반드시 한 번은 막힙니다. 예외들 사이에는 부모-자식 관계가 있습니다. JAVA2에서 배운 상속이 여기서 등장합니다. Exception이 부모이고, ArithmeticException 같은 것들이 그 자식입니다.

넓은 부모를 위에 두면 어떻게 될까요.

try {
    System.out.println(10 / 0);
} catch (Exception e) {                    // 부모를 맨 위에 → 모든 예외를 여기서 잡아 버림
    System.out.println("오류 발생");
} catch (ArithmeticException e) {          // 컴파일 오류! 여기까지 올 일이 없음
    System.out.println("0으로 나누기");
}

부모인 Exception이 모든 예외를 먼저 삼켜 버리니, 아래 catch는 영원히 실행될 수 없는 코드가 됩니다. 자바 컴파일러는 이걸 잡아내고 오류를 냅니다. 다형성 편에서 본 "부모 타입은 자식을 담을 수 있다"는 성질이 여기서도 그대로 작동하는 셈입니다.

그래서 규칙은 하나입니다. 좁은 예외를 위에, 넓은 예외를 아래에. 맨 아래 Exception은 "그 밖의 모든 경우"를 받는 자리로 쓰면 됩니다.

throw — 예외를 내가 직접 일으키기

지금까지는 자바가 알아서 예외를 발생시켰습니다. 그런데 자바 문법상으로는 멀쩡한데 우리 규칙에는 어긋나는 값이 있습니다. 나이가 음수라거나, 주문 수량이 0이라거나 하는 것들이죠. 자바는 이걸 오류로 보지 않습니다. 우리가 직접 알려 줘야 합니다.

이럴 때 쓰는 것이 throw입니다.

void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("나이는 0보다 작을 수 없습니다");
    }
    this.age = age;
}

throw new 예외종류(메시지) 형태로 쓰면, 그 자리에서 예외가 발생한 것과 똑같이 동작합니다. JAVA2 캡슐화 편에서 setter로 잘못된 값을 걸러냈던 것 기억하시나요. 그때는 메시지만 출력하고 넘어갔지만, throw를 쓰면 호출한 쪽이 반드시 알아채도록 강하게 알릴 수 있습니다.

throws — 처리를 부른 쪽에 넘기기

이름이 한 글자 차이라 헷갈리지만 하는 일은 전혀 다릅니다.

throw는 예외를 발생시키고 throws는 처리 책임을 넘긴다는 비교 그림

그림 1. throw는 메서드 안에서 예외를 발생시키고, throws는 선언부에서 처리 책임을 넘깁니다.

throws는 메서드 선언부에 붙여서 "이 메서드는 이런 예외가 날 수 있는데, 여기서는 처리하지 않겠다"고 예고하는 표시입니다.

void readFile() throws Exception {     // 여기서 안 잡음 → 부른 쪽이 처리
    // 파일을 여는 코드 (예외가 날 수 있음)
}

public static void main(String[] args) {
    try {
        readFile();                    // 부른 쪽에서 try-catch로 처리
    } catch (Exception e) {
        System.out.println("파일을 열 수 없습니다.");
    }
}

왜 이런 게 필요할까요. 예외가 난 자리에서 항상 좋은 대응을 할 수 있는 건 아니기 때문입니다. 파일 읽기 메서드는 "파일이 없다"는 사실만 알 뿐, 그래서 프로그램을 종료할지 다른 파일을 쓸지는 모릅니다. 그 판단은 부른 쪽이 하는 게 맞습니다. throws는 그 판단을 넘기는 통로입니다.

직접 실행해 보기

아래 코드를 돌려 보세요. 그다음 divide(10, 0)의 0을 다른 숫자로 바꿔 보고, checkAge에 음수 대신 정상 나이를 넣어 흐름이 어떻게 달라지는지 확인해 보시기 바랍니다.

다중 catch와 throw
public class Main {
  // 규칙 위반이면 직접 예외를 던짐
  static void checkAge(int age) {
      if (age < 0) {
          throw new IllegalArgumentException("나이는 0보다 작을 수 없습니다");
      }
      System.out.println("나이 확인: " + age + "살");
  }

  public static void main(String[] args) {
      int[] nums = {10, 20, 30};

      try {
          System.out.println(nums[0] / 0);   // ArithmeticException 발생
          System.out.println(nums[5]);       // 여기까지 오지 않음
          checkAge(-5);
      } catch (ArithmeticException e) {
          System.out.println("계산 오류: " + e.getMessage());
      } catch (ArrayIndexOutOfBoundsException e) {
          System.out.println("범위 오류: " + e.getMessage());
      } catch (Exception e) {               // 넓은 예외는 맨 아래
          System.out.println("그 밖의 오류: " + e.getMessage());
      }

      // throw가 실제로 어떻게 걸리는지 따로 확인
      try {
          checkAge(-5);
      } catch (IllegalArgumentException e) {
          System.out.println("규칙 위반: " + e.getMessage());
      }

      System.out.println("프로그램은 계속 진행됩니다.");
  }
}

📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.

한 장 정리

항목내용
다중 catchtry 하나에 catch 여러 개, 위에서부터 타입이 맞는 곳에서 걸림
순서 규칙좁은 예외를 위에, 넓은 Exception을 아래에 (거꾸로면 컴파일 오류)
묶어 잡기대응이 같으면 세로 막대로 여러 예외를 한 catch에
throw메서드 안에서 예외를 직접 발생시킴 (규칙 위반 알리기)
throws메서드 선언부에서 처리 책임을 부른 쪽에 넘김

이것만 기억하면 됩니다. catch는 여러 개 달아 오류 종류별로 다르게 대응하되, 좁은 것을 위에 넓은 것을 아래에 두어야 합니다. 그리고 이름이 비슷한 두 키워드는 역할이 정반대입니다. throw는 예외를 만들어 던지는 것, throws는 처리를 넘긴다고 예고하는 것입니다.

미니 퀴즈

개념을 제대로 잡았는지 직접 풀어 봅니다. 정답은 눌러야 나옵니다.

미니 퀴즈

OX 2문제 + 4지선다 3문제


포켓코딩(JAVA3) 2편 · 예제 출처: 강사 개인 GitHub 저장소. 다음 편: 제네릭 — 타입을 나중에 정하는 틀 만들기.

PANCO_IT

IT 지식을 정리하는 개발자

관련 글