프로그램이 비정상적으로 종료되거나 오작동을 하는 경우 프로그램 에러 또는 오류라고 한다. 발생 시점에 따라 컴파일 에러 또는 런타임 에러로 나눌 수 있다. 컴파일 에러는 컴파일 시 발생하는 에러이고 런타임 에러는 프로그램 실행 도중 발생하는 에러이다.
소스코드를 컴파일 하면 컴파일러가 소스코드(.java)에 대해 기본적인 검사를 수행하여 오류가 있는지를 알려준다. 이 때 오타나 잘못된 구문, 자료형이 잘못되어 있는 경우 컴파일 에러가 난다.
컴파일을 성공적으로 마치고 난다고 해서 프로그램 실행 시 에러가 나지 않는 것은 아니다. 실행 중에 발생하는 에러를 런타임에러라고 하고, 런타임에러는 또 에러(error)와 예외(Exception), 두 가지로 나뉜다.
에러는 메모리 부족(OutOfMemoryError)나 스택오버플로우(StackOverFlowException)와 같이 발생하면 복구할 수 없는 심각한 오류이고, 예외는 ArithmeticException, IndexOutOfBoundException과 같이 수습이 가능한 덜 심각한 오류이다.
예외도 두가지로 나뉘게 되는데 사용자들의 동작과 같은 외부의 영향으로 발생하는 Exception과 프로그래머의 실수에 의해서 발생되는 예외인 RuntimeException으로 나뉜다. Exception이 발생할 가능성이 있는 문장들에 대해 예외처리를 해주지 않으면 컴파일 조차 되지 않는 반면(checked 예외), RuntimeException의 경우 프로그래머의 실수에 의해 발생하기 때문에 예외처리를 강제하지 않는다.(unchecked 예외)
try {
// 예외가 발생할 가능성이 있는 문장넣기
} catch(예외가 발생할 것 같은 Exception e) {
// 예외 처리
} catch ...
try문, catch문 안에 또 다른 try-catch문이 작성될 수 있다.
ArithmeticException의 예를 들면,
package sample;
public class Sample {
public static void main(String[] args) {
for (int i=0; i<10; i++) {
try {
int result = 100 / (int) (Math.random() * 10);
System.out.println(result);
System.out.println("성공");
} catch (ArithmeticException ae) {
System.out.println(0);
System.out.println("예외");
} catch (Exception e) {
}
}
}
}
100을 0~9 중 무작위로 수를 뽑아 나누는 프로그램이다. 0이 걸릴 경우 catch문으로 넘어가 0을 출력하도록 했다.
try 문에서 예외가 발생하면 그 이후의 문장은 수행되지 않고 바로 catch문으로 이동하게 된다. 따라서 0으로 나누어지지 않은 수 다음에는 성공이 출력되고, 0으로 나누어진 수 다음은 예외가 출력된다.
여러 catch문 중 발생한 예외의 종류와 일치하는 단 한개의 catch문만 수행된다. 이 코드에서는 ArithmeticException이 일어났고 그에 해당하는 첫번째 catch문만을 수행하게 된다. 만약 어떤 종류의 예외가 발생하더라도 예외처리를 해주려면 마지막 catch문에 Exception 타입의 참조변수를 선언하면 된다. 하지만 전에 학습했던 것처럼 메소드를 오버라이딩하는 경우 선언된 예외의 개수 혹은 예외의 상속관계에 따라 오버라이딩이 안되는 경우가 있으니 예외처리를 할 때 이런점까지 고려를 해야 한다.
예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 예외에 대한 정보가 담겨 있으며, getMessage 메서드와 printStackTrace 메서드를 호출해서 정보를 얻을 수 있다.
package sample;
public class Sample {
public static void main(String[] args) {
try {
System.out.println(1);
System.out.println(2);
System.out.println(0 / 0);
System.out.println(4);
} catch (ArithmeticException ae) {
ae.printStackTrace();
System.out.println("예외 메세지 : " + ae.getMessage());
}
System.out.println(5);
}
}
1
2
예외 메세지 : / by zero
5
java.lang.ArithmeticException: / by zero
at sample.Sample.main(Sample.java:8)
여타 다른 메서드와 마찬가지로 인스턴스.메서드 의 방법으로 호출해서 사용하면 된다. getMessage()를 통해 발생한 예외 클래스의 인스턴스에 저장된 메세지를 리턴할 수 있고, printStackTrace()를 통해 호출스택에 있던 메서드의 정보와 예외 메세지를 화면에 출력해 준다.
예외 종류에 따라 여러개의 catch문을 작성할 수도 있지만, 하나의 catch문에 | 기호를 사용하여 한꺼번에 작성할 수도 있다.
try {
....
} catch(ExceptionA | ExceptionB | ExceptionC... e) {
...
}
Exception e = new Exception("예외 문구 작성");
throw e;
throw new Exception();
예외를 발생시키고 싶은 곳에 위의 두가지 방법 중 하나를 사용하면 예외를 고의로 발생시킬 수 있다. 생성자에 문자열을 적으면 예외 발생시 예외 클래스의 인스턴스에 메세지로 저장되며, getMessage()로 리턴할 수 있다.
고의로 예외를 발생시키는 이유는 뭘까? 이유까지는 교재에 나와있지는 않지만 추측은 해볼 수 있다. 다음 파트에 throw를 배우게 되는데 예외를 호출할 메서드로 미루는 것이다. 즉, 예외가 발생한 메서드가 아닌 호출할 메서드에서 예외처리를 하도록 하는 것이다. 근데 예외를 한 메서드에서만 처리할 수도 있지만 두 메서드에서 나눠서 예외를 처리하는 경우도 있다고 한다. 아직까진 이해가 잘안되지만 예외를 나눠서 처리하는 경우, 예외가 발생한 메서드에서 예외처리를 한 후, catch문에서 다시 예외를 고의로 발생시켜서 호출할 메서드로 다시 그 예외를 던지고, 호출할 메서드에서 또 예외처리를 하도록 할 수 있다.
package sample;
public class Sample {
public static void main(String[] args) {
try {
methodA();
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.getMessage());
}
}
static void methodA() throws Exception{
methodB();
}
static void methodB() throws Exception{
throw new Exception("예외 발생!");
}
}
package sample;
import java.io.IOException;
import java.io.PrintWriter;
public class Sample {
public static void main(String[] args) throws IOException {
PrintWriter pw = null;
try {
pw = new PrintWriter("C:\\Users\\kcs91\\Desktop\\workspace\\java\\src\\sample.txt");
for (int i = 1; i < 11; i++) {
String data = i + " 번째 줄입니다.";
pw.println(data);
}
} catch (IOException ie) {
ie.printStackTrace();
} finally {
pw.close();
}
}
}
finally {
try {
if (pw != null)
pw.close();
} catch (IOException ie) {
ie.printStackTrace();
}
}
package sample;
import java.io.IOException;
import java.io.PrintWriter;
public class Sample {
public static void main(String[] args) throws IOException {
try (PrintWriter pw = new PrintWriter("C:\\Users\\kcs91\\Desktop\\workspace\\java\\src\\sample.txt")){
for (int i = 1; i < 11; i++) {
String data = i + " 번째 줄입니다.";
pw.println(data);
}
} catch (IOException ie) {
ie.printStackTrace();
}
}
}
class A Extends Exception {
A(String msg) {
super(msg)
}
}
package sample;
public class Sample {
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의 try문에 고의로 예외를 발생시켰지만 method1에서 예외가 발생했다고 가정하자. catch문에서 다시 예외 클래스의 인스턴스 e를 던진다. 그럼 예외가 해결된 것이 아니므로 호출될 메서드에서 예외를 해결해야한다는 것을 명시하기 위해 method1 선언부에 throw Exception을 작성한다.
main 메서드에서 예외를 처리해야하므로 try-catch구문을 사용해서 try문에 method1을 호출한다.