7) 예외2 - 예외의 종류

dev-mage·2022년 10월 30일
0

Hello Java World!

목록 보기
21/32
post-thumbnail

예외의 발생과 checked, unchecked 및 사용자 정의 예외

Throwable 클래스의 메서드

모든 에러와 예외의 부모 클래스는 Throwable 클래스이다. 예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨 있는데 이는 Throwable 클래스의 getMessage와 printStackTrace 메서드로 접근할 수 있다. try-catch문으로 예외를 처리하여 예외가 발생해도 비정상적으로 종료되지 않도록 해주는 동시에 앞서 말한 메서드로 예외의 발생 원인을 알 수 있다.

  • getMessage(): 발생한 예외 클래스의 인스턴스에 저장된 메시지를 얻을 수 있음.
  • printStackTrace(): 예외 발생 당시의 호출 스택에 있었던 메서드의 정보와 예외 메시지를 화면에 출력.
    public class ExceptionFlow {
        public static void main(String[] args) {
            try {
                System.out.println(0/0);
            } catch (ArithmeticException ae) {
                ae.printStackTrace();
            }
        }
    }

throw: 예외 발생시키기

키워드 throw를 사용하면 프로그래머가 고의로 예외를 발생시킬 수 있다.

  1. 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체 생성.
  2. 키워드 throw를 이용해서 예외 발생.
public class ExceptionFlow {
    public static void main(String[] args) {
        try {
            // Exception e = new Exception("고의로 발생시킨 예외");
            // throw e;
            throw new Exception("고의로 발생시킨 예외"); // 한 줄로 작성 가능
        } catch (Exception e) {
            System.out.println("에러 메세지: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

예외 클래스의 객체를 생성할 때 생성자의 매개변수에 String을 넣어 주면 해당 String이 객체의 메시지로 저장된다. 이 메시지는 getMessage()로 접근할 수 있다.

throws: 메서드에 예외 선언하기

예외를 처리하는 방법엔 try-catch문 외에 메서드와 같이 선언하는 방법도 있다. 메서드에 예외를 선언하려면 메서드 선언부에 키워드 throws를 사용해 메서드 내에서 발생할 수 있는 예외를 작성하면 된다.

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

만약 Exception 클래스를 선언한다면 해당 메서드는 모든 종류의 예외가 발생할 수 있다는 의미가 된다.

void method() throws Exception { ... } // 어떤 예외든 발생 가능

따라서 메서드에 예외를 선언할 경우 선언한 예외의 자식 타입 예외까지 발생할 수 있다는 점을 주위해야 한다. 이렇게 메서드의 선언부에 예외를 작성함으로써 메서드를 사용하기 위해 어떤 예외들이 처리되어야 하는지 쉽게 알 수 있다.

checked 와 unchecked 예외

예외는 다음과 같이 크게 2가지로 나뉜다.

  1. checked 예외
    • Exception 클래스와 자식 클래스들(RuntimeException 제외).
    • 발생 예측 가능 → The Catch or Specify Requirement를 따라야함.
      • 반드시 try-catch문이나 throws로 예외 처리를 해주어야 함.
      • 예외 처리하지 않으면 컴파일 불가능.
  2. unchecked 예외
    • Error 클래스 및 RuntimeException 클래스와 그 자식 클래스들.
    • 프로그램 실행 중 발생하기 때문에 예측하기 어려움 → 예외 처리를 강제하지 않음.

사용자 정의 예외

기존에 정의된 예외 클래스 이외에 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다. 보통은 Exception 클래스 또는 RuntimeException 클래스로부터 상속 받는 클래스를 만든다.

public class UserException extends Exception { ... }

호출 스택과 예외 처리

public class ExceptionFlow {
    static void method1() throws Exception {
        method2();
    }
    static void method2() throws Exception {
        throw new Exception();
    }
    public static void main(String[] args) throws Exception {
        method1();
    }
}

위의 코드를 실행하면 다음과 같은 결과와 호출 스택을 확인할 수 있다.

  • 예외가 발생했을 때 3개의 메서드(main, method1, method2)가 호출 스택에 있었음.
  • 예외가 발생한 곳은 method2 메서드.
  • main 메서드가 method1()을, method1()은 method2()를 호출

  • 흐름
    1. method2()에서 예외가 발생(throw new Exception();)
    2. try-catch문으로 예외 처리를 해주지 않았으므로 종료되면서 자신을 호출한 method1()에게 예외를 넘김.
    3. method1()에서도 따로 예외 처리를 해주지 않았으므로 종료되면서 main 메서드에게 예외를 넘김.
    4. main 메서드에서도 예외 처리를 하지 않아 main 메서드가 종료되면서 프로그램도 예외로 인해 비정상적으로 종료

이처럼 예외가 발생한 메서드에서 예외 처리를 하지 않고 자신을 호출한 메서드에게 예외를 넘겨줄 순 있지만 예외를 처리하지 않고 단순히 전달만 하는 것이다. 결국 어느 한 곳에서는 반드시 try-catch문으로 예외 처리를 해주어야 한다.

다음은 사용자로부터 입력 받은 이름으로 파일을 생성하는 코드이다. generateFile 메서드는 입력값이 유효하지 않으면 예외를 발생시키고 유효하다면 해당 값으로 파일을 생성한다.

package file;

import java.io.File;
import java.util.Scanner;

public class FileGenerator {
    static File generateFile(String fileName) throws Exception {
        if(fileName == null || fileName.equals("")) {
            throw new Exception("파일 이름이 유효하지 않습니다.");
        }
        File file = new File("./src/file/files/", fileName);
        file.createNewFile();
        return file;
    }

    public static void main(String[] args) {
        System.out.println("파일 이름을 입력하세요.");
        Scanner scanner = new Scanner(System.in);
        try {
            String fileName = scanner.nextLine();
            File file = generateFile(fileName);
            System.out.println(file.getName() + " 파일이 성공적으로 생성되었습니다.");
        } catch (Exception e) {
            System.out.println(e.getMessage() + " 다시 실행해 주시기 바랍니다.");
        } finally {
            scanner.close();
        }
    }
}

하지만 예외는 generateFile 메서드를 호출한 main 메서드의 try-catch문에서 처리한다. 이는 generateFile 메서드가 작업을 수행하는 과정에서 문제가 생기자 예외를 발생시켜 main 메서드에게 알리는 것과 같다. 만약 generateFile 메서드가 예외를 처리하면 main 메서드에서는 예외를 발생했다는 사실을 알 수 없다. 예외가 발생했을 때 예외가 발생한 메서드 내에서 자체적으로 처리해도 되는 경우 메서드 내에 try-catch문을 넣어서 처리하고 메서드 내에서 자체적으로 해결이 안되는 경우(파일 이름을 다시 받아와야 하는 경우)에는 예외를 선언해서 호출한 메서드가 처리하도록 해야 한다.

try-with-resources문

위 예제를 보면 스캐너를 생성해 사용자 입력을 받은 후 finally 블럭에서 닫아주는 것을 알 수 있다. Scanner 클래스는 데이터를 입력 받으려고 할 때 사용되는데 마치 수도꼭지로 물(자원)을 틀고 더 이상 쓸 일이 없으면 잠그는 것처럼 입력(자원)을 다 받았다면 이를 해제해 주어야 한다. 때문에 자원을 낭비하지 않기 위해 무조건 실행되는 finally 블럭에서 스캐너를 닫아 준 것이다. Java 7부터는 이를 쉽게 해주는 try-with-resources문이 도입되었다. try-with-resources문을 활용하면 다음과 같이 코드를 작성할 수 있다.

public static void main(String[] args) {
        System.out.println("파일 이름을 입력하세요.");
        try (Scanner scanner = new Scanner(System.in)) {
            String fileName = scanner.nextLine();
            File file = generateFile(fileName);
            System.out.println(file.getName() + " 파일이 성공적으로 생성되었습니다.");
        } catch (Exception e) {
            System.out.println(e.getMessage() + " 다시 실행해 주시기 바랍니다.");
        }
    }

try-with-resources문은 try 블럭을 벗어나면 close()를 호출해 자동으로 자원을 해제해 준다.

  • 참고: 자동으로 close()를 호출하려면 AutoCloseable를 구현한 객체여야 한다.

예외 되던지기(exception re-throwing)

한 메서드에서 발생할 수 있는 예외가 여럿인 경우, 일부는 해당 메서드 내에서 자체적으로 처리하고 나머지는 선언부에 지정한 뒤 인위적으로 예외를 다시 발생시켜 호출한 메서드에서 나누어 처리할 수 있다. 이것을 예외 되던지기라고 한다. 하나의 예외에 대해 예외가 발생한 메서드와 이를 호출한 메서드 양쪽 모두에서 처리해줘야 할 작업이 있을 때 사용된다.

public class ExceptionFlow {
    static void method1() throws Exception {
        try {
            throw new Exception();
        } catch (Exception e) {
            System.out.println("method1 메서드에서 예외 처리됨.");
            throw e; // 다시 예외 발생
        }
    }
    public static void main(String[] args) {
        try {
            method1();
        } catch (Exception e) {
            System.out.println("main 메서드에서 예외 처리됨.");
        }
    }
}

반환값이 있는 return문의 경우, catch 블럭에도 return문이 있어야 한다. 예외가 발생해도 값을 반환해야 하기 때문이다.

연결된 예외(chained exception)

한 예외가 다른 예외를 발생하게 할 수 있다. 예외 A가 예외 B를 발생시켰다면 A를 B의 원인 예외(cause exception)라고 한다. 먼저 예외를 생성한 후 Throwable 클래스의 initCause 메서드로 다른 예외를 원인 예외로 설정한다. 그리고 throw로 생성한 예외를 던진다.

  • Throwable initCause(Throwable cause): 지정한 예외를 원인 예외로 등록
  • Throwable getCause(): 원인 예외를 반환
try {
    throw new CauseException("원인 예외 발생");
} catch (CauseException ce) {
    ResultException re = new ResultException("예외 발생");
    re.initCause(ce);
    throw re;
}

발생한 예외를 그냥 처리하지 않고 원인 예외로 등록해서 다시 예외를 발생시키는 이유는 여러 가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위함이다.

예를 들어 작은 카페가 있고 메뉴로는 커피, 차, 에이드를 판매한다고 하자. 커피는 3000원, 차는 3500원, 에이드는 4000원이다.

public class JavaCafe {
    static final String[][] MENU = {{"coffee", "3000"}, {"tea", "3500"}, {"ade", "4000"}};
}

만약 없는 메뉴를 주문하거나 있는 메뉴를 주문해도 지불한 금액이 맞지 않는 경우를 확인하기 위해 checkMenu()와 checkMoney()를 정의했다. 주문이 실패한 경우 각각 NoSuchMenuException과 WrongAmountOfMoneyException이 발생한다.

public class NoSuchMenuException extends Exception{ ... }

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

public class WrongAmountOfMoneyException extends Exception { ... }

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

public class JavaCafe {
    static final String[][] MENU = {
				{"coffee", "3000"}, {"tea", "3500"}, {"ade", "4000"}
		};

    static int checkMenu(int order) throws NoSuchMenuException {
        switch (order) {
            case 1 :
                return Integer.parseInt(MENU[0][1]);
            case 2 :
                return Integer.parseInt(MENU[1][1]);
            case 3 :
                return Integer.parseInt(MENU[2][1]);
        }
        throw new NoSuchMenuException("없는 메뉴");
    }
    static boolean checkMoney(int price, int money) 
			throws WrongAmountOfMoneyException {
        if(price == money) {
            return true;
        }
        throw new WrongAmountOfMoneyException("잘못된 금액");
    }
}

두 경우 모두 주문 실패에 해당하기 때문에 FailedOrderException으로 묶으려 하는데 NoSuchMenuException과 WrongAmountOfMoneyException의 조상으로 FailedOrderException을 지정하면 실제로 발생한 예외가 어떤 것인지 알 수 없고 두 예외의 상속 관계를 변경해야 하는 문제가 생긴다. 이 때 원인 예외를 포함하게 한다면 두 예외는 상속 관계가 아니어도 상관 없게 된다.

import java.util.Scanner;
public class JavaCafe {
    static final String[][] MENU = {{"coffee", "3000"}, {"tea", "3500"}, {"ade", "4000"}};

    static void order() throws FailedOrderException {
        System.out.println("---- 메뉴 ----");
        for(int i = 0; i < MENU.length; i++) {
            System.out.printf("%d. %s: %s원 %n", i+1, MENU[i][0], MENU[i][1]);
        }
        System.out.print("선택: ");
        try (Scanner scanner = new Scanner(System.in)) {
            int price = checkMenu(scanner.nextInt());
            System.out.print("지불 금액: ");
            int money = scanner.nextInt();
            if (checkMoney(price, money)) {
                System.out.println("주문 완료");
            }
        } catch (NoSuchMenuException | WrongAmountOfMoneyException e) {
            FailedOrderException fe = new FailedOrderException("주문 실패: " + e.getMessage());
            fe.initCause(e);
            throw fe;
        }

    }
    static int checkMenu(int order) throws NoSuchMenuException {
        switch (order) {
            case 1 :
                return Integer.parseInt(MENU[0][1]);
            case 2 :
                return Integer.parseInt(MENU[1][1]);
            case 3 :
                return Integer.parseInt(MENU[2][1]);
        }
        throw new NoSuchMenuException("없는 메뉴");
    }
    static boolean checkMoney(int price, int money) throws WrongAmountOfMoneyException {
        if(price == money) {
            return true;
        }
        throw new WrongAmountOfMoneyException("잘못된 금액");
    }

    public static void main(String[] args) {
        try {
            order();
        } catch (FailedOrderException fe) {
            System.out.println(fe.getMessage());
            System.out.println("다시 주문해 주세요.");
        }
    }
}
  • 없는 메뉴를 주문한 경우
  • 지불한 금액이 잘못된 경우
  • 주문에 성공한 경우

References

0개의 댓글