프로그램 오류
프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우가 있다. 이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다.
에러의 발생 시점에 따라 컴파일 에러와 런타임 에러로 나눌 수 있는데, 컴파일 에러(compile-time error)는 컴파일할 때 발생하는 에러이고 프로그램의 실행 도중에 발생하는 에러를 런타임 에러(runtime error)라고 한다.
소스코드를 컴파일하면 컴파일러가 소스코드(*.java)에 대해 오타나 잘못된 구문, 자료형 체크 등의 기본적인 검사를 수행하여 오류가 있는지 알려 준다.
하지만 컴파일을 에러 없이 성공적으로 마쳤다고 해서 프로그램의 실행 시에도 에러가 발생하지 않는 것은 아니다. 컴파일러가 소스코드의 기본적인 사항은 컴파일 시에 모두 걸러 줄 수는 있지만, 실행 도중에 에러에 의해서 잘못된 결과를 얻거나 프로그램이 비정상적으로 종료될 수 있다.
런타임 에러를 방지하기 위해서는 프로그램의 실행 도중 발생할 수 있는 모든 경우의 수를 고려하여 이에 대한 대비를 하는 것이 필요하다. 자바에서는 실행 시(rumtime) 발생할 수 있는 프로그램 오류를 에러(error)와 예외(exception), 두 가지로 구분하였다.
에러(error)는 메모리 부족(OutOfMemoryError)이나 스택오버플로유(StackOverflowError)와 같이 일단 발생하면 복구할 수 없는 심각한 오류이고, 예외(exception)는 발생하더라도 수습될 수 있는 비교적 덜 심각한 것이다.
에러가 발생하면, 프로그램의 비정상적인 종료를 막을 길이 없지만, 예외는 발생하더라도 프로그래머가 이에 대한 적절한 코드를 미리 작성해 놓음으로써 프로그램의 비정상적인 종료를 막을 수 있다.
예외 클래스의 계층구조
자바에서는 실행 시 발생할 수 있는 오류(Exception과 Error)를 클래스로 정의하였다. Exception과 Error클래스 역시 모든 클래스의 조상인 Object 클래스이다.
RuntimeException 클래스
- 주로 프로그래머의 실수에 의해 발생
- ArrayIndexOutOfBoundException : 배열의 범위 초과.
- NullPointerException : 값이 null인 참조 변수의 멤버 호출.
- ClassCastException : 클래스 간의 형 변환 불가.
- ArithmeticException : 정수를 0으로 나눔.
Exception 클래스
- 주로 외부의 영향으로 발생
- FileNotFoundException : 존재하지 않는 파일에 접근.
- ClassNotFoundException : 클래스의 이름을 잘못 적음.
- DataFormatException : 데이터 형식이 잘못됨.
예외 처리하기 try-catch문
예외처리(exception handling)란,
프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것
프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것
예외를 처리하지 못하면, 프로그램은 비정상적으로 종료되며, 처리되지 못한 예외는 JVM의 예외 처리기(UncaughtExceptionHandler)가 받아서 예외의 원인을 화면에 출력한다.
try-catch
public class Test {
public static void main(String[] args) {
try {
try {
} catch (Exception e) {
}
} catch (Exception e) {
try {
} catch (Exception e) { // 변수 e 중복 선언 에러.
}
} // try-catch 끝
}
}
하나의 try 블록 다음에는 여러 종류의 예외를 처리할 수 있도록 하나 이상의 catch 블록이 올 수 있으며, 발생한 예외의 종류와 일치하는 catch블록이 없으면 예외는 처리되지 않는다.
하나의 메서드 내에 여러 개의 try-catch문이 사용될 수 있으며, try블록 또는 catch블록에 또 다른 try-catch문이 포함될 수 있다. catch블록 내의 코드에서도 예외가 발생할 수 있기 때문이다.
try-catch문에서의 흐름
try블록 내에서 예외가 발생한 경우,
- 발생한 예외와 일치하는 catch블록이 있는지 확인한다.
- 일치하는 catch블록을 찾게 되면, 그 catch블록 내의 문을 수행하고 try-catch문을 빠져나간다.
만일 일치하는 catch블록을 찾지 못하면, 예외는 처리되지 못한다.
try블록 내에서 예외가 발생하지 않은 경우,
- catch블록을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속한다.
public class Test {
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) {
System.out.println(5);
}
System.out.println(6);
}
}
1
2
3
5
6
try블록에서 예외가 발생했기 때문에 try블록을 바로 벗어나서 4가 출력되지 않는다.
예외의 발생과 catch블럭
catch블록의 괄호 () 내에는 처리하고자 하는 예외와 같은 타입의 참조 변수 하나를 선언해야 한다.
예외가 발생하면, 발생한 예외에 해당하는 클래스의 인스턴스가 만들어진다.
첫 번째 catch블록부터 차례로 내려가면서 catch블록의 괄호() 내에 선언된 참조 변수의 종류와 생성된 예외 클래스의 인스턴스에 instanceof연산자를 이용해서 검사하게 되는데, 검사 결과가 true인 catch블록을 만날 때까지 검사는 계속된다.
검사 결과가 true인 catch블록을 찾게 되면 블록에 있는 문장들을 모두 수행한 후에 try-catch문을 빠져나가고 예외는 처리되지만, 검사 결과가 true인 catch블록이 하나도 없으면 예외는 처리되지 않는다.
public class Test {
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("ae is ArithmeticException's instance");
}
System.out.println("ArithmeticException");
} catch (Exception e) { //ArithmeticeException을 제외한 모든 예외 처리.
System.out.println("Eception");
}
System.out.println(6);
}
}
1
2
3
ae is ArithmeticException's instance
ArithmeticException
6
첫 번째 검사에서 일치하는 catch블록을 찾았기 때문에 두 번째 catch블록은 검사하지 않게 된다.
이처럼, try-catch문의 마지막 Exception클래스 타입의 참조 변수를 선언한 catch블록을 이용하면, 어떤 종류의 예외가 발생하더라도 이 catch블록에 의해 처리되도록 할 수 있다.
printStackTrace()와 getMessage()
예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨 있으며, getMessage()와 printStackTrace()를 통해서 이 정보들을 얻을 수 있다.
catch블록의 괄호()에 선언된 참조 변수를 통해 이 인스턴스에 접근할 수 있다.
printStackTrace()
예외 발생 당시의 호출 스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.
getMessage()
발생한 예외 클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.
public class Test {
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) {
ae.printStackTrace();
System.out.println("예외메시지 : " + ae.getMessage());
} catch (Exception e) { //ArithmeticeException을 제외한 모든 예외 처리.
System.out.println("Eception");
}
System.out.println(6);
}
}
1
2
3
예외메시지 : / by zero
6
java.lang.ArithmeticException: / by zero
at Test.main(Test.java:7)
예외는 try-catch문에 의해 처리되었으며 프로그램은 정상적으로 종료되었다.
그 대신 ArithmeticException인스턴스의 printStackTrace()를 사용해서, 호출 스택(Call Stack)에 대한 정보와 예외 메시지를 출력하였다.
예외 발생시키기
키워드 throw를 사용해서 프로그래머가 고의로 예외를 발생시킬 수 있다.
- 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든다.
- Exception e = new Exception(" ~~ ");
- 키워드 throw를 이용해서 예외를 발생시킨다.
- throw e;
메서드에 예외 선언하기
void method() throws Exception1, Exception2, ... {
...
}
메서드의 선언부에 키워드 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 된다.
메서드 내에서 발생할 가능성이 있는 예외를 선언부에 명시하여 이 메서드를 사용하는 쪽에서 이에 대한 처리를 하도록 강요하기 때문에, 프로그래머들의 짐을 덜어 주는 것은 물론이고 보다 견고한 프로그램 코드를 작성할 수 있도록 도와준다.
메서드에 예외를 선언할 때 일반적으로 RuntimeException클래스들은 적지 않는다. 이들은 메서드 선언부의 throws에 선언한다고 해서 문제가 되진 않지만, 보통 반드시 처리해주어야 하는 예외들만 선언한다.
사실 예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라, 자신을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다.
finally블록
finally블록은 예외의 발생 여부에 상관없이 실행되어야 할 코드를 포함시킬 목적으로 사용된다.
public class Test {
public static void main(String[] args) {
Test.method1();
System.out.println("method1() 수행 ");
}
static void method1() {
try {
System.out.println("method1() 호출");
return;
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("method1()의 finally블럭 실행");
}
}
}
try블록에서 return문이 실행되는 경우에도 finally블록의 문이 먼저 실행된 후에, 메서드를 종료한다.