프로그램이 실행 중 어떤 원인에 의해서 오작동 하거나 비정상적으로 종료되는 경우가 있다.
프로그램 에러 또는 오류 라고 한다.발생시점에 따라 다음과 같이 나눌 수 있다.
컴파일 에러 - 컴파일 시에 발생하는 에러
런타임 에러 - 실행 시에 발생하는 에러
논리적 에러 - 실행은 되지만, 의도와 다르게 동작하는 것
컴파일 에러는 컴파일러가 알려 준 에러들을 모두 수정해서 해결할 수 있다.
런타임 에러를 방지하기 위해서는 프로그램의 실행도중 발생할 수 있는 모든 경우의 수를 고려하여 이에 대한 대비를 하는 것이 필요하다. 자바에서는 실행 시(runtime) 발생할 수 있는 프로그램 오류를 에러(error) 와 예외(exception), 두 가지로 구분하였다.
에러(error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
예외(exception) : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류
예외클래스 계층도는 다음과 같다.

Exception클래스들 - 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외
RuntimeException클래스들 - 프로그래머의 실수로 발생하는 예외
프로그램의 실행도중에 발생하는 에러는 어쩔 수 없지만, 예외는 프로그래머가 이에 대한 처리를 미리 해주어야 한다.
예외처리(exception handling)의
정의 - 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것
목적 - 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것
발생한 예외를 처리하지 못하면, 프로그램은 비정상적으로 종료되며, 처리되지 못한 예외(uncaught exception)는 JVM의 예외처리기(UncaughtExceptionHandler) 가 받아서 예외의 원인을 화면에 출력한다.
예외를 처리하기 위한 try-catch 문의 구조는 다음과 같다.
try {
// 예외가 발생할 가능성이 있는 문장들을 넣는다.
} catch (Exception1 e1) {
// Exception1이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch (Exception2 e2) {
// Exception2이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch (ExceptionN eN) {
// ExceptionN이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
}
▶ try 블럭 내에서 예외가 발생한 경우,
1. 발생한 예외와 일치하는 catch 블럭이 있는지 확인한다.
2. 일치하는 catch 블럭을 찾게 되면, 그 catch 블럭 내의 문장들을 수행하고 전체 try-catch 문을 빠져나가서 그 다음 문장을 계속해서 수행한다. 만일 일치하는 catch 블럭을 찾지 못하면, 예외는 처리되지 못한다.
▶ try 블럭 내에서 예외가 발생하지 않은 경우,
1. catch 블럭을 거치지 않고 전체 try-catch 문을 빠져나가서 수행을 계속한다.
catch 블럭의 괄호() 내에는 처리하고자 하는 예외와 같은 타입의 참조변수 하나를 선언해야한다.
첫 번쨰 catch 블럭부터 차례로 내려가면서 괄호() 내에 선언된 참조변수의 종류와 생성된 예외클래스의 인스턴스에 instanceof 연산자를 이용해서 검사한다.
public class ExceptionEx7 {
public static void main(String[] args) {
System.out.println(1);
System.out.println(2);
try {
System.out.println(3);
System.out.println(0 / 0); // 0으로 나눠서 ArithmeticException을 발생시킨다.
System.out.println(4); // 실행되지 않는다.
} catch (ArithmeticException ae) {
if (ae instanceof ArithmeticException)
System.out.println("true");
System.out.println("ArithmeticException");
} catch (Exception e) { // ArithmeticException을 제외한 모든 예외가 처리된다.
System.out.println("Exception");
}
System.out.println(6);
}
}
/* 실행결과
1
2
3
true
ArithmeticException
6
*/
참고로, 연속된 catch 블럭에서는 최하위 자손 부터 순서대로 써줘야 한다. 예를 들면, 다음과 같다.
try {
...
} catch (Exception e) {
...
} catch (ArithmeticException ae) {
...
}
// 위처럼이 아니라 아래와 같이
try {
...
} catch (ArithmeticException ae) {
...
} catch (Exception e) {
...
}
조상 예외클래스를 먼저 만나면 자손 예외클래스인지 알 방법이 없기 때문이다. 정확한 원인을 알 수 없을수도 있다는 뜻이다.
printStackTrace() - 예외발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.
getMessage() - 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.
ex)
try {
...
} catch (ArithmeticException ae) {
ae.printStackTrace();
System.out.println("예외메시지 : " + ae.getMessage());
}
JDK 1.7 부터 여러 catch 블럭을 | 기호를 이용해서, 합칠 수 있다.
try {
...
} catch (ExceptionA e) {
e.printStackTrace();
} catch (ExceptionB e2) {
e2.printStackTrace();
}
// '|' 기호 적용
try {
...
} catch (ExceptionA | ExceptionB e) {
e.printStackTrace();
}
| 기호로 연결된 예외 클래스가 조상과 자손의 관계 에 있다면 컴파일 에러가 발생한다.
키원드 throw 를 사용해서 프로그래머가 고의로 예외를 발생시킬 수 있다.
1. 먼저, 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든 다음
Exception e = new Exception("고의로 발생시켰음");
2. 키워드 throw를 이용해서 예외를 발생시킨다.
throw e;
public class ExceptionEx9 {
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();
}
System.out.println("프로그램이 정상 종료되었음.");
}
}
✔ 컴파일러가 예외처리를 확인하지 않는 RuntimeException 클래스들은 unchecked예외 라고 부르고, 예외처리를 확인하는 Exception 클래스들은 checked예외 라고 부른다.
try-catch 문을 사용하는 것 외에, 예외를 메서드에 선언하는 방법이 있다.
메서드의 선언부에 키워드 throws 를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 된다.
여러 개일 경우에는 쉼표(,)로 구분한다.
void method() throws Exception1, Exception2, ... ExceptionN { // 메서드의 내용 }
자바에서는 메서드를 작성할 때 메서드 내에서 발생할 가능성이 있는 예외를 메서드의 선언부에 명시하여 이 메서드를 사용하는 쪽에서는 이에 대한 처리를 하도록 강요하기 때문에, 프로그래머들의 짐을 덜어 주는 것은 물론이고 보다 견고한 프로그램 코드를 작성할 수 있도록 도와준다.
public class ExceptionEx12 {
public static void main(String[] args) throws Exception {
method1(); // 같은 클래스내의 static멤버이므로 객체 생성없이 직접 호출가능.
}
static void method1() throws Exception {
method2();
}
static void method2() throws Exception {
throw new Exception();
}
}
method2()에서 throw new Exception(); 문장에 의해 예외가 강제적으로 발생했으나 try-catch 문으로 예외처리를 해주지 않았으므로,
method2()는 종료되면서 예외를 호출한 method1()에게 넘겨준다. method1()에서도 역시 예외처리를 해주지 않았으므로 종료되면서 main 메서드에게 예외를 넘겨준다.
그러나 main 메서드에서 조차 예외처리를 해주지 않았으므로 main 메서드가 종료되어 프로그램이 예외로 인해 비정상적으로 종료되는 것이다.
이처럼 예외가 발생한 메서드에서 예외처리를 하지 않고 자신을 호출한 메서드에게 예외를 넘겨줄 수는 있지만, 이것으로 예외가 처리된 것은 아니고 예외를 단순히 전달만 하는 것이다.
결국 어느 한 곳에서는 반드시 try-catch문으로 예외처리를 해주어야 한다.
try {
// 예외가 발생할 가능성이 있는 문장들을 넣는다.
} catch(Exception e1) {
// 예외처리를 위한 문장을 적는다.
} finally {
// 예외의 발생여부에 관계없이 항상 수행되어야 하는 문장들을 넣는다.
// finally 블럭은 try-catch 문의 맨 마지막에 위치
}
이 구문은 주로 15장 입추력(I/O) 과 관련된 클래스를 사용할 때 유용한데, 이런게 있다는 정도만 가볍게 봐뒀다가 참고하자.
자원(resources) 이 반환되기 때문이다.try {
fis = new FileInputStream("score.dat");
dis = new DataInputStream(fis);
...
} catch (IOException ie) {
ie.printStackTrace();
} finally {
dis.close(); // 작업 중에 예외가 발생하더라도, dis가 닫히도록 finally 블럭에 넣음
}
위 코드에서 문제는 close()가 예외를 발생시킬 수 있다는데 있다. 그래서 finally 부분을 아래와 같이 하기도 한다.
} finally {
try {
if(dis != null)
dis.close();
} catch (IOException ie) {
ie.printStackTrace();
}
}
그러나 코드가 복잡해져서 별로 보기 좋지 않다. 이 점을 개선하기 위해서 try-with-resources 문이 추가된 것이다.
try (FileInputStream fis = new FileInputStream("score.dat");
DataInputStream dis = new DataInputStream(fis)) {
while(true) {
score = dis.readInt();
System.out.println(score);
sum += score;
}
} catch (EOFException e) {
System.out.println("점수의 총합은 " + sum + "입니다.");
} catch (IOException ie) {
ie.printStackTrace();
}
try-with-resources 문의 괄호()안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()를 호출하지 않아도 try 블럭을 벗어나는 순간 자동적으로 close()가 호출된다.
기존의 정의된 예외 클래스 외에 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다.
class MyException extends Exception {
MyException(String msg) { // 문자열을 매개변수로 받는 생성자
super(msg); // 조상인 Exception 클래스의 생성자를 호출한다.
}
}
필요하다면, 멤버변수나 메서드를 추가할 수 있다.
예전과 달리 요즘은 프로그래밍 환경이 달라진 만큼 필수적으로 처리해야만 할 것 같았던 예외들이 선택적으로 처리해도 되는 상황으로 바뀌는 경우가 종종 발생하고 있다.
그래서 필요에 따라 예외처리의 여부를 선택할 수 있는 unchecked예외 가 강제적인 checked예외 보다 더 환영받고 있다.
한 메서드에서 발생할 수 있는 예외가 여럿인 경우,
발생한 메서드 와 호출한 메서드, 양쪽에서 처리하도록 할 수 있다.-> 예외를 처리한 후에 인위적으로 다시 발생시키는 방법을 통해서 가능한데, 이것을 예외 되던지기(exception re-throwing) 라고 한다.
public class ExceptionEx17 {
public static void main(String[] args) {
try {
method1();
} catch (Exception e) {
System.out.println("main메서드에서 예외가 처리되었습니다.");
}
}
static void method1() throws Exception {
try {
throw new Exception();
} catch (Exception e) {
System.out.println("method1메서드에서 예외가 처리되었습니다.");
throw e; // 다시 예외를 발생시킨다.
}
}
}
/* 실행결과
method1메서드에서 예외가 처리되었습니다.
main메서드에서 예외가 처리되었습니다.
*/
한 예외가 다른 예외를 발생시킬 수도 있다.
예를 들어, 예외 A가 예외 Bfmf qkftodtlzuTekaus, A를 B의 원인 예외(cause exception) 라고 한다.
try {
startInstall(); // SpaceException 발생
copyFiles();
} catch (SpaceException e) {
InstallException ie = new InstallException("설치중 예외발생"); // 예외 생성
ie.initCause(e); // InstallException의 원인 예외를 SpaceException으로 지정
throw ie; // InstallException을 발생시킨다.
} catch (...) {
...
}
발생한 예외를 그냥 처리하면 될 텐데, 원인 예외로 등록해서 다시 예외를 발생시키는 이유?
✔ 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서 !
🤔🤔 흠.. 이 부분은 잘 모르겠다. 굳이 사용할 일이 있을지도 모르겠고... 언젠가 쓸 일이 있다면 다시 끄적이러 와야겠다.