예외처리(exception handling)란?
프로그램이 오작동 하거나 비정상적으로 종료되는 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다. 이를 발생시점에 따라 분류하는데 컴파일 시에 발생하는 에러를 '컴파일 에러(compile-time error)', 런타임 시에 발생하는 에러를 '런타임 에러(runtime error)', 실행은 되지만 의도와는 다르게 동작하는 경우를 '논리적 에러(logical error)'라고 한다.
- 컴파일 에러 : 컴파일 시에 발생하는 에러
- 런타임 에러 : 실행 시에 발생하는 에러
- 논리적 에러 : 실행은 되지만, 의도와 다르게 동작하는 것
런타임 에러는 프로그램이 멈추거나 비정상적인 종료를 초래하기 때문에 실행도중 발생할 수 있는 모든 경우의 수를 대비해야 한다. 자바에서는 실행 시 발생할 수 있는 프로그램 오류를 '에러(error)'와 '예외(exception)'으로 구분하였다.
구분 기준은 복구 가능성의 여부이다. 에러는 메모리 부족(OutOfMemoryError)이나 스택오버플로우(StackOverflowError)와 같이 복구 불가능한 오류이고, 예외는 복구 가능한 오류이다. 예외가 발생하는 경우 이에 대한 처리를 통해 프로그램의 비정상적인 종료를 방지할 수 있다. 즉, 예외처리는 말 그대로 프로그램의 비정상적인 종료를 방지하기 위해 복구 가능한 예외에 대해 적절한 코드를 작성하는 것이다.
- 에러 : 프로그램 코드에 의해 수습이 불가능한 오류
- 예외 : 프로그램 코드에 의해 수습이 가능한 오류
자바에서는 발생할 수 있는 오류를 클래스로 정의해두었다. Exception 클래스와 Error 클래스로 나뉘며 Error 클래스에는 수습 불가능한 오류에 대해 정의되어 있고, Exception 클래스에는 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외가 정의되어 있다. 특히 RuntimeException 클래스에는 개발자의 실수로 발생하는 예외들이 정의되어 있다.

※ 자바의 모든 클래스들의 최상위 조상은 Object 클래스이다.
예외처리하기
예외처리 방법에는 try-catch문(try-catch-finally문)과 메서드에서 예외를 선언하는 방법이 있다. 우선 예외처리를 위해 try-catch문(try-catch-finally문)을 알아보자.
try {
//예외 발생 가능성이 있는 문장
} catch (Exception1 e) {
// Exception1이 발생한 경우, 이를 처리하기 위한 문장
} catch (Exception2 e) {
// Exception2가 발생한 경우, 이를 처리하기 위한 문장
} catch (ExceptionN e) {
// ExceptionN이 발생한 경우, 이를 처리하기 위한 문장
} finally {
// 예외 발생여부에 관계없이 항상 수행돼야하는 문장
// try-catch문 맨 마지막에 위치해야 함
// 수행시킬 문장이 없는 경우 생략 가능
}
하나의 try 블럭 이후 복수의 catch 블럭이 올 수 있으며 마지막에 선택적으로 finally 블럭이 올 수 있다. 복수의 catch 블럭이 존재하는 경우 이 중 예외의 종류와 일치하는 단 한 개의 catch 블럭만 수행된다.
<try-catch문 실행 순서>
- 예외 발생 X -> finally 블럭이 있으면 해당 내용 수행 -> try-catch문 탈출 -> 이후 내용 수행
- 예외 발생 O -> 예외와 같은 catch 블럭 있음 -> finally 블럭이 있으면 해당 내용 수행 -> try-catch문 탈출 -> 이후 내용 수행
- 예외 발생 O -> 예외와 같은 catch 블럭 없음 -> finally 블럭이 있으면 해당 내용 수행 -> 에러 발생
try-catch문의 간단한 특징들에 대해서 알아보자. 우선 catch 블럭의 예외는 범위가 구체적인 예외 클래스부터 작성해야 한다.
//에러. Exception이 이미 NullPointerException을 포함하기 때문
try {
//...
} catch (Exception e) {
//...
} catch (NullPointerException e) {
//...
}
//수정
try {
//...
} catch (NullPointerException e) {
//...
} catch (Exception e) {
//...
}
예외가 여러 개 발생한 경우 가장 최근에 발생한 예외가 출력된다.
try {
//첫번째 에러 발생
System.out.println(0 / 0);
} catch (ArithmeticException e) {
//...
} finally {
try {
int[] arr = new int[1];
//두번째 에러 발생
System.out.println(arr[2]);
} catch (NullPointerException e) {
//...
}
}
<출력된 에러>

예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨있으며, getMessage()와 printStackTrace()를 통해 해당 정보를 얻을 수 있다.
try {
//...
} catch (Exception e) {
e.getMessage(); // 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻음
e.printStackTrace(); // 예외발생 당시의 호출스택에 있었던 메서드의 정보와 예외 메시지를 화면에 출력
}
Java7부터 catch 블럭을 '|' 기호를 이용해 합칠 수 있게 되었다. 예외처리 내용이 같은 경우 중복된 코드 제거가 가능하다.
//before
try {
//...
} catch (NullPointerException e) {
System.out.println("error");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("error");
}
//after
try {
//...
} catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
System.out.println("error");
}
//알아두기
//한 예외 클래스가 다른 예외 클래스의 상위 클래스인 경우 에러가 발생
//RuntimeException이 NullPointerException이 발생한 경우도 포함하기 때문
try {
//...
} catch (RuntimeException | NullPointerException e) {
System.out.println("error");
}
이제 메서드에서 예외를 선언하는 방법에 대해 알아보자. 이 경우 메서드 선언부에 키워드 throws를 사용해 메서드 내에서 발생할 수 있는 예외를 적어준다. 예외가 여러 개인 경우 ','를 이용해 구분한다.
//구조
void method() throws Exception1, Exception2, Exception3 {
//...
}
//예제
void method() throws ArithmeticException, NullPointerException {
System.out.println(0/0); //ArithmeticException
System.out.println("이후 내용"); //출력 안됨
//...
}
이 방법은 예외를 처리하는 방법이라기 보다는 해당 메서드에서 발생할 수 있는 예외를 명시하고 해당 메서드를 호출한 메서드에 책임을 전가하는 방식이다.
※ 메서드에 예외를 선언할 때 일반적으로 RuntimeException 클래스들은 적지 않는다.
※ 발생한 예외가 처리되지 않는 경우, JVM의 '예외처리기(UncaughtExceptionHandler)'가 예외를 받아 원인을 화면에 출력한다.
※ return 값의 경우 finally > try, catch의 우선순위를 가진다.
try-with-resources문
Java7부터 추가됐으며 try-catch문의 변형된 형태로 할당된 자원을 자동으로 반환해준다. 다음은 책의 예제 코드이다.
FileInputStream fis = null;
DataInputStream dis = null;
try {
fis = new FileInputStream("파일명");
dis = new DataInputStream(fis);
...
} catch (IOException e) {
e.printStackTrace();
} finally {
//자원 반환을 위한 코드 + 예외 처리
try {
if (dis != null)
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
파일 데이터를 읽는 중 예외가 발생한 경우 할당된 자원을 반환할 필요가 있다. 위의 코드는 자원 반환 과정을 finally에서 처리해주는 형태지만, 문제는 close() 메서드는 예외처리가 필요하기 때문에 중첩된 try-catch문을 적용해야 한다. 하지만 이 경우에 close() 메서드에서 예외가 발생하게 되면 이전 try 구문에서 발생했던 예외는 무시된다는 문제가 발생한다.
이런 점을 보완하기 위해 등장한 것이 try-catch-resources문이다. try에 괄호가 추가된 형태로 괄호 내에는 객체를 생성하는 문장을 넣어 try 블럭을 벗어난 순간 생성한 객체들에 대해 자동적으로 close() 메서드가 호출되게 한다.
try (FileInputStream fis = new FileInputStream("파일명");
DataInputStream dis = new DataInputStream(fis)) {
...
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("end");
}
※ try 괄호 안에 문장은 ';'로 구분한다.
※ try 괄호 안에 변수를 선언할 수 있으며 선언한 변수는 try 블럭 내에서만 사용 가능하다.
※ 객체에 대해 close() 메서드가 자동 호출되기 위해선 해당 객체의 클래스가 AutoCloseable 인터페이스를 구현하고 있어야 한다.
※ try-catch-resources문의 경우 예외가 두 개 이상 발생한 경우, 두 번째 예외부터는 '억제된(suppressed)'이라는 의미의 머리말과 함께 출력된다.
사용자정의 예외
일반적으로 Exception 클래스나 RuntimeException 클래스를 상속받아 구현한다. 책의 예제 코드는 다음과 같다.
public class MyException extends Exception {
public MyException(String message) {
super(message);
}
}
Exception 클래스는 'checked예외'로 예외처리를 필수적으로 요구하며, RuntimeException 클래스는 'unchecked예외'로 예외처리를 선택적으로 한다. Exception 클래스를 상속받아 사용자정의 예외를 구현하는 경우 불필요한 예외처리로 코드의 복잡성이 증가할 수 있기 때문에 이를 고려하여 작성해야 한다.
※ 꼭 필요한 경우가 아니면 새로운 예외를 정의하는 것 보다 기존의 예외를 잘 활용하자.
※ 'check예외'를 처리하지 않은 경우 컴파일 에러가 발생하며, 'unchecked예외'를 처리하지 않은 경우 런타임 에러가 발생한다.
예외 던지기
throw 키워들 이용해 고의로 예외를 발생시킬 수 있다. 간단한 코드를 통해 이해해보자.
//try-catch문
public void method() {
try {
Exception e = new Exception("test 예외");
throw e;
//한 줄 정의
//throw new Exception("test 에러");
} catch (Exception e) {
e.printStackTrace();
}
}
//메서드에 선언
public void method() throws Exception {
Exception e = new Exception("test 예외");
throw e;
//한 줄 정의
//throw new Exception("test 에러");
}
throw 키워드를 이용하여 예외에 대해 복수의 메서드에서 예외를 처리되도록 할 수 있다.
public void method1() {
try {
method2();
} catch (Exception e) {
System.out.println("method1 예외 처리");
}
}
public void method2() throws Exception {
try {
throw new Exception("test 예외");
} catch (Exception e) {
System.out.println("method2 예외 처리");
throw e;
}
}
※ 호출한 메서드로 예외를 전달하는 경우 return 문을 생략할 수 있다.
한 예외가 다른 예외를 발생시킬 수 있으며, 다른 예외를 발생시키는 예외를 '원인 예외(cause exception)'라고 한다. 이러한 방식은 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위함이다. 책의 예제 코드는 다음과 같다.
public void method1() {
try {
method2();
} catch (ProgramRunException e) {
e.getCause(); //getCause() : 원인 예외를 반환
}
}
public void method2() throws ProgramRunException {
try {
//...
} catch (IOException e) {
//원인에러 : IOException
ProgramRunException pe = new ProgramRunException("프로그램 실행 실패");
pe.initCause(e); //initCause() : 지정한 예외를 원인 예외로 등록
throw pe;
}
}
원인 예외를 지정함으로써 얻을 수 있는 이점은 다음과 같다.
- 상속관계가 달라도 다른 예외를 포함시킬 수 있다.
- 실제 발생한 예외를 특정할 수 있다.
- 'checked예외'를 'unchecked예외'로 변경함으로써 예외처리를 단순화할 수 있다.
<예외처리 단순화 과정>
//before
public void method() throws ProgramRunException {
if (!programRunFailed()) {
throw new ProgramRunException("에러 발생");
}
}
//after
public void method() {
if (!programRunFailed()) {
//RuntimeException 생성자 이용
throw new RuntimeException(new ProgramRunException("예외 발생"));
//RuntimeException re = new RuntimeException();
//re.initCause(new ProgramRunException("예외 발생"));
//throw re;
}
}
Q&A
Q. 예외처리란?
A. 프로그램의 비정상적인 종료를 방지하기 위해 복구 가능한 예외에 대해 적절한 코드를 작성하는 것이다.
Q. 예외와 에러의 차이는?
A. 예외는 프로그램 코드 작성을 통해 처리가 가능한 오류를 의미하고, 에러는 프로그램 코드 작성을 통해 처리가 불가능한 오류를 의미한다.
Q. try-catch-resources문이란?
A. try-catch문에 자동 자원 반환 기능을 추가하여 코드의 복잡성을 줄인 형태이다.
Q. 'checked예외'와 'unchecked예외'의 차이는?
A. 'checked예외'는 예외처리를 필수로 요구하고, 'unchecked예외'는 예외처리를 선택적으로 요구한다.
참고자료
- Java의 정석
'개념서 > Java' 카테고리의 다른 글
| [Java] 내부 클래스 (0) | 2022.06.25 |
|---|---|
| [Java] 인터페이스 (0) | 2022.06.23 |
| [Java] 다형성 (0) | 2022.06.17 |
| [Java] 제어자 (0) | 2022.06.13 |
| [Java] package와 import (0) | 2022.06.10 |
댓글