예외처리
예기치 않게 발생하는 에러에 대응할 수 있는 코드를 미리 사전에 작성하여 프로그램의 비정상적인 종료를 방지하고, 정상적인 실행 상태를 유지하기 위한 기능이 예외처리(Exception Handling) 이다.
에러가 발생하는 원인은 수없이 다양하나 다음의 몇 가지 예를 들 수 있다.
- 사용자의 입력 오류
- 네트워크 연결 끊김
- 디스크 메모리 공간 부족 등 물리적 한계
- 개발자의 코드 에러
- 존재하지 않는, 유효하지 않는 파일 불러오기
위의 원인을 크게 내부적인 요인과 외부적인 요인으로 구분할 수 있다.
먼저 대표적인 외부 요인으로 하드웨어의 문제, 네트워크의 연결 끊김, 사용자 조작 오류 등이 있고, 내부 요인으로는 대표적으로 주로 오늘 우리가 초점을 맞춰 학습하게 될 개발자의 코드 작성 에러를 언급할 수 있다.
개발자가 프로그래밍 하면서 발생시키기 쉬운 대표적인 몇 가지 오류를 살펴보자.
public class ErrorTest {
public static void main(String[] args) {
BufferedReader notExist = new BufferedReader(new FileReader("없는 파일"));
notExist.readLine();
notExist.close();
}
}
위의 코드는 존재하지 않는 파일을 실행하고자 하고있다. 코드를 실행하기도 전에 IDE에서 빨간줄로 표시해주는 것도 확인할 수 있다.
실제로 코드를 실행하면 아래와 같은 에러 메세지를 확인할 수 있다.
java: unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown
FileNotFoundException이라는 이름으로 확인할 수 있는 것처럼 실제로 존재하지 않는 파일을 불러오려 시도할 때 발생하는 예외이다.
public class ErrorTest {
public static void main(String[] args) {
int[] array = {2,4,6};
System.out.println(array[3]);
}
}
위의 코드에서는 2까지 밖에 없는 배열의 범위에서 범위밖에 있는 3 값을 불러오고자 하고 있다. 위의 코드를 실행하면 아래와 같은 에러가 발생한다.
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
at ErrorTest.main(ErrorTest.java:4)
ArrayIndexOutOfBoundsException 이라는 이름에서 유추할 수 있듯 배열의 범위를 벗어난 값을 불러오고자 시도 할 때 발생하는 예외이다.
자바에서는 발생 시점에 따라 에러를 논리적 에러(Logical Error), 컴파일 에러(Compile Time Error), 런타임 에러(Run Time Error)로 구분할 수 있다.
컴파일 에러
컴파일 에러는 이름 그대로 컴파일 할 때 발생하는 에러이다. 주로 세미콜론 생략, 오탈자, 잘못된 자료형, 잘못된 포맷 등 문법적인 문제를 가리키는 신택스(syntax) 오류로부터 발생하기 때문에 신택스 에러(Syntax Error) 라고도 부른다.
public class ErrorTest {
public static void main(String[] args) {
int i;
for (i= 1; i<= 5; i++ {
System.out.println(i);
}
}
}
위의 코드는 실행 전에도 빨간 줄로 에러를 확인 할 수 있겠지만 실행한다면 에러가 발생하여 아래와 같은 내용을 볼 수 있다.
java: ')' expected
for 문의 조건을 설정하는 곳에서 닫는 괄호가 빠진 문제였다. 이처럼 컴파일 에러는 상대적으로 발견하기 쉬우며 어렵지 않게 해결할 수 있다.
런타임 에러
런타임 에러는 이름 그대로 실행하는 과정, 즉 런타임 시에 발생하는 에러를 말한다.
public class RuntimeErrorTest {
public static void main(String[] args) {
System.out.println(4 * 4);
System.out.println(4 / 0); // 예외 발생
}
}
//출력값
16
Exception in thread "main" java.lang.ArithmeticException: / by zero
at RuntimeErrorTest.main(RuntimeErrorTest.java:5)
위의 코드 예제를 입력해보면 실행 전에는 아무 문제 없는 것 처럼 보인다. 하지만 실행해보면 아래의 출력값에서 확인할 수 있듯이 ArithmeticException 예외가 발생한다. ArithmeticException은 특정 숫자를 0으로 나누었을 때 발생하는 예외이다.
에러와 예외
지금까지는 모든 문제를 에러라고 불렀지만 사실 엄밀히 따지면 자바에서 잠재적으로 발생할 수 있는 프로그램 오류를 크게 에러(error)와 예외(exception)로 구분하고 있다.
에러란 한번 발생하면 복구하기 어려운 수준의 심각한 오류를 의미하고 대표적으로 메모리 부족(OutOfMemoryError)과 스택오버플로우(StackOverflowError)등이 있다.
반면 예외는 잘못 사용 또는 코딩으로 인한 상대적으로 미약한 수준의 오류로서 코드 수정등을 통해 수습이 가능한 오류를 지칭한다.
예외 클래스의 상속 계층도
자바에서는 예외가 발생하면 예외 클래스로부터 객체를 생성하여 해당 인스턴스를 통해 예외 처리를 한다. 자바의 모든 에러와 예외 클래스는 아래 이미지에서 확인할 수 있듯이 Throwable 클래스로부터 확장되며, 모든 예외의 최고 상위클래스는 Exception 클래스이다. 모든 예외의 최고 상위 클래스인 Exception 클래스는 다시 크게 일반 예외 클래스와 실행 예외 클래스로 나눌 수 있다.
일반 예외 클래스 (Exception)
런타임 시 발생하는 RuntimeException 클래스와 그 하위 클래스를 제외한 모든 Exception 클래스와 그 하위 클래스들을 가리킨다. 컴파일러가 코드 실행 전에 예외 처리 코드 여부를 검사한다고 하여 checked 예외 라고 부르기도 한다. 주로 잘못된 클래스명(ClassNotFoundException)이나 데이터 형식(DataFormatException) 등 사용자편의 실수로 발생하는 경우가 많다.
실행 예외 클래스 (Runtime Exception)
앞서 언급한 런타임 시 발생하는 Runtime Exception 클래스와 그 하위 클래스를 지칭한다. 컴파일러가 예외 처리 코드 여부를 검사하지 않는다는 의미에서 unchecked 예외라 부르기도 한다. 주로 개발자의 실수로 발생하는 경우가 많고 자바 문법 요소와 관련이 있다.
클래스간 형변환 오류(ClassCastException), 벗어난 배열 범위 지정(ArrayIndexOutOfBoundsException) 값이 null 인 참조변수 사용(NullPointerException)등이 있다.
try - catch 문
자바에서 예외 처리는 try-catch 블록을 통해 구현할 수 있다. 그 기본 구조는 다음과 같다.
try {
// 예외가 발생할 가능성이 있는 코드를 삽입
}
catch (ExceptionType1 e1) {
// ExceptionType1 유형의 예외 발생 시 실행할 코드
}
catch (ExceptionType2 e2) {
// ExceptionType2 유형의 예외 발생 시 실행할 코드
}
finally {
// finally 블록은 옵셔널
// 예외 발생 여부와 상관없이 항상 실행
}
먼저 try 블록 안에는 예외가 발생할 가능성이 있는 코드를 삽입한다. 만약 작성한 코드가 예외 없이 정상적으로 실행되면 아래 catch 블록은 실행되지 않고 finally 블록이 실행된다. finally 블록은 옵션으로 필수적으로 포함되지 않아도 되지만, 만약 포함될 때는 예외 발생 여부와 상관없이 항상 실행된다.
catch 블록은 예외가 발생할 때 실행되는 코드로 여러 종류의 예외를 처리할 수 있다. 모든 예외를 받을 수 있는 Exception 클래스 하나로 처리도 가능하며 각기 다른 예외를 하나 이상의 catch 블록을 사용하여 처리할 수 있다. 만약 catch 블록이 여러개인 경우, 일치하는 하나의 catch 블록만이 실행되고 예외처리 코드가 종료되거나 finally 블록으로 넘어가게 된다. 만약 일치하는 블록을 찾지 못하면 예외는 처리되지 못한다.
public class RuntimeExceptionTest {
public static void main(String[] args) {
System.out.println("[소문자 알파벳을 대문자로 출력하는 프로그램]");
printMyName("abc"); // (1)
printMyName(null); // (2) 넘겨주는 매개변수가 null인 경우 NullPointerException 발생
System.out.println("[프로그램 종료]");
}
static void printMyName(String str) {
String upperCaseAlphabet = str.toUpperCase();
System.out.println(upperCaseAlphabet);
}
}
//출력값
[소문자 알파벳을 대문자로 출력하는 프로그램]
ABC //(3) 정상 실행
Exception in thread "main" java.lang.NullPointerException // (4) 예외 발생!
at RuntimeExceptionTest.printMyName(RuntimeExceptionTest.java:11)
at RuntimeExceptionTest.main(RuntimeExceptionTest.java:6)
위의 코드는 프로그램을 실행하면 소문자 알파벳을 대문자로 바꿔주는 간단한 프로그램 코드이다. printMyName 메서드를 먼저 확인해보면 문자열 타입의 매개변수 str을 받아 대문자로 변환해 준 다음에 해당 문자열을 출력하고 있다. 이러한 순서에 따라 (1) 번의 코드 라인에서 printMyName 메서드를 호출하면, (3) 번의 출력값에서 확인할 수 있는 것처럼 해당 문자열이 대문자로 바뀌어 잘 출력된다.
하지만 (2)번의 코드 라인에서 동일한 메서드를 호줄한다면 어떻게 될까?
넘겨주는 매개변수의 값이 null이기 때문에 해당 메서드가 호출되면 (4) 번의 출력값처럼 NullPointerException 예외가 발생하게 된다.
public class RuntimeExceptionTest {
public static void main(String[] args) {
try {
System.out.println("[소문자 알파벳을 대문자로 출력하는 프로그램]");
printMyName(null); // (1) 예외 발생
printMyName("abc"); // 이 코드는 실행되지 않고 catch 문으로 이동
}
catch (ArithmeticException e) {
System.out.println("ArithmeticException 발생!"); // (2) 첫 번째 catch문
}
catch (NullPointerException e) { // (3) 두 번째 catch문
System.out.println("NullPointerException 발생!");
System.out.println("e.getMessage: " + e.getMessage()); // (4) 예외 정보를 얻는 방법 - 1
System.out.println("e.toString: " + e.toString()); // (4) 예외 정보를 얻는 방법 - 2
e.printStackTrace(); // (4) 예외 정보를 얻는 방법 - 3
}
finally {
System.out.println("[프로그램 종료]"); // (5) finally문
}
}
static void printMyName(String str) {
String upperCaseAlphabet = str.toUpperCase();
System.out.println(upperCaseAlphabet);
}
}
// 출력값
[소문자 알파벳을 대문자로 출력하는 프로그램]
NullPointerException 발생!
e.getMessage: null
e.toString: java.lang.NullPointerException
[프로그램 종료]
java.lang.NullPointerException
at RuntimeExceptionTest.printMyName(RuntimeExceptionTest.java:20)
at RuntimeExceptionTest.main(RuntimeExceptionTest.java:7)
위의 코드는 앞서 확인한 try-cathc 문을 사용하여 예외처리를 하고자 하는 코드이다. 하나씩 순차적으로 살펴보자.
- 예외 발생
try 문 안에서 순차적으로 잘 실행되는 코드가 null 값을 매개변수로 넘긴 printMyName 메서드가 호출되는 부분에서 예외가 발생했다. 따라서 다음 코드인 printMyName("abc")는 호출되지 않고 예외가 발생한 시점에서 catch 문으로 넘어간다. - 첫 번째 catch문
발생한 예외가 NullPointerException인데 첫 번째 catch 문에서 조건으로 받는 예외는 ArithmeticException 이므로 예외처리가 되지 않고 그냥 지나간다. 참고로 이때 검사는 instanceOf 연산자를 통해 생성된 예외 클래스의 인스턴스가 조건과 일치하는지 판단한다.
한 가지 유의할 점은 예외가 발생하면 catch 블록은 위에서부터 순차적으로 검사를 진행하기 때문에 구체적인 예외 클래스인 하위클래스를 먼저 위에 배치하여 상위 예외 클래스가 먼저 실행되지 않도록 방지하는 것이 좋다. - 두 번째 catch문
발생한 예외와 일치하는 조건이므로 해당 catch 문의 코드블록이 순차적으로 실행된다. (4)번에서 확인할 수 있는 것처럼 예외가 발생할 때 생성되는 예외 객체로부터 해당 에러에 대한 정보를 얻을 수 있다. - finally 문
앞서 언급핸던 것처럼 finally 문은 꼭 포함되어야 하는 문법은 아니지만, 만약에 포함이 되어있다면 예외 발생 여부와 관계없이 무조건 실행된다.
예외 전가
try-catch 문 외에 예외를 호출한 곳으로 다시 예외를 떠넘기는 방법이 있다. 이를 위해 메서드의 선언부 끝에 아래와 같이 throws 키워드와 발생할 수 있는 예외들을 쉼표로 구분하여 나열해주면 된다.
반환타입 메서드명(매개변수, ...) throws 예외클래스1, 예외클래스2, ... {
...생략...
}
아래의 예제 코드를 한번 살펴보도록 하자
public class ThrowExceptionTest {
public static void main(String[] args) {
try {
throwException();
} catch (ClassNotFoundException e) {
System.out.println(e.getMessage());
}
}
static void throwException() throws ClassNotFoundException, NullPointerException {
Class.forName("java.lang.StringX");
}
}
//출력값
java.lang.StringX
위의 코드를 살펴보면 try-catch 문에서 throwException 메서드가 호출되지만, 잘못된 코드 작성으로 인한 런타임 에러가 발생한다. 이렇게 예외가 발생한 경우 우리는 잠재적으로 예외가 발생할 수 있는 곳에서 try-catch 문을 작성하여 예외처리를 해주었다.
하지만 위의 코드의 경우는 throws 키워드를 사용하여 해당 예외를 발생한 메서드 안에서 처리하지 않고 메서드를 호출한 곳으로 다니 떠넘기고 있다. 따라서 이제 예외처리의 책임은 throwException 메서드가 아닌 main 메서드가 지게 된다.
예외를 의도적으로 발생시키기
throw 키워드를 사용하면 의도적으로 예외를 발생시킬 수 있다.
public class ExceptionTest {
public static void main(String[] args) {
try {
Exception intendedException = new Exception("의도된 예외 만들기");
throw intendedException;
} catch (Exception e) {
System.out.println("고의로 예외 발생시키기 성공!");
}
}
}
//출력값
고의로 예외 발생시키기 성공!
'공부 > 전자컴퓨터공학' 카테고리의 다른 글
Java 애너테이션(Annotation) 이란? 자바 기초 배우기 (0) | 2024.05.30 |
---|---|
Java 컬렉션 프레임워크란? 자바 기초 배우기 (0) | 2024.05.30 |
Java 제네릭(Generic) 이란? 자바 기초 배우기 (0) | 2024.05.28 |
Java 열거형(Enum) 이란? 자바 기초 배우기 (0) | 2024.05.28 |
Java 추상화(Abstraction) 란? 자바 기초 배우기 (1) | 2024.05.28 |