예외의 발생과 checked, unchecked 및 사용자 정의 예외
모든 에러와 예외의 부모 클래스는 Throwable 클래스이다. 예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨 있는데 이는 Throwable 클래스의 getMessage와 printStackTrace 메서드로 접근할 수 있다. try-catch문으로 예외를 처리하여 예외가 발생해도 비정상적으로 종료되지 않도록 해주는 동시에 앞서 말한 메서드로 예외의 발생 원인을 알 수 있다.
public class ExceptionFlow {
public static void main(String[] args) {
try {
System.out.println(0/0);
} catch (ArithmeticException ae) {
ae.printStackTrace();
}
}
}
키워드 throw
를 사용하면 프로그래머가 고의로 예외를 발생시킬 수 있다.
new
를 이용해서 발생시키려는 예외 클래스의 객체 생성.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()로 접근할 수 있다.
예외를 처리하는 방법엔 try-catch문 외에 메서드와 같이 선언하는 방법도 있다. 메서드에 예외를 선언하려면 메서드 선언부에 키워드 throws
를 사용해 메서드 내에서 발생할 수 있는 예외를 작성하면 된다.
void method() throws Exception1, Exception2, ... , ExceptionN {
...
}
만약 Exception 클래스를 선언한다면 해당 메서드는 모든 종류의 예외가 발생할 수 있다는 의미가 된다.
void method() throws Exception { ... } // 어떤 예외든 발생 가능
따라서 메서드에 예외를 선언할 경우 선언한 예외의 자식 타입 예외까지 발생할 수 있다는 점을 주위해야 한다. 이렇게 메서드의 선언부에 예외를 작성함으로써 메서드를 사용하기 위해 어떤 예외들이 처리되어야 하는지 쉽게 알 수 있다.
예외는 다음과 같이 크게 2가지로 나뉜다.
throws
로 예외 처리를 해주어야 함.기존에 정의된 예외 클래스 이외에 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다. 보통은 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();
}
}
위의 코드를 실행하면 다음과 같은 결과와 호출 스택을 확인할 수 있다.
throw new Exception();
) 이처럼 예외가 발생한 메서드에서 예외 처리를 하지 않고 자신을 호출한 메서드에게 예외를 넘겨줄 순 있지만 예외를 처리하지 않고 단순히 전달만 하는 것이다. 결국 어느 한 곳에서는 반드시 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문을 넣어서 처리하고 메서드 내에서 자체적으로 해결이 안되는 경우(파일 이름을 다시 받아와야 하는 경우)에는 예외를 선언해서 호출한 메서드가 처리하도록 해야 한다.
위 예제를 보면 스캐너를 생성해 사용자 입력을 받은 후 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()를 호출해 자동으로 자원을 해제해 준다.
한 메서드에서 발생할 수 있는 예외가 여럿인 경우, 일부는 해당 메서드 내에서 자체적으로 처리하고 나머지는 선언부에 지정한 뒤 인위적으로 예외를 다시 발생시켜 호출한 메서드에서 나누어 처리할 수 있다. 이것을 예외 되던지기라고 한다. 하나의 예외에 대해 예외가 발생한 메서드와 이를 호출한 메서드 양쪽 모두에서 처리해줘야 할 작업이 있을 때 사용된다.
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문이 있어야 한다. 예외가 발생해도 값을 반환해야 하기 때문이다.
한 예외가 다른 예외를 발생하게 할 수 있다. 예외 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("다시 주문해 주세요.");
}
}
}