Live Study 9주차 - 예외 처리


예외처리 방법

예외처리

자바에서는 어떤 메서드를 호출할 때 개발자가 예측하지 못한 예외(Exception)가 발생할 수 있는데, 이 때 이 상황을 제어할 수 있도록 돕는 기능이 예외처리(Exception Handling)이라고 한다.

프로그램을 중단시키기 때문에 에러(Error)와 헷갈릴수 있는데, 예외와 에러의 차이는 아래에서 다시정리해보겠다.

아래의 코드를 보자.

1
2
3
4
5
6
7
8
9
10
public class StudyHalle {
public static void main(String[] args){
StudyHalle study = new StudyHalle();
System.out.println(study.divide(10,0));
System.out.println("예외처리를 위한 예제 코드");
}
public float divide(int num1, int num2){
return num1/num2;
}
}

100으로 나누려고 하여 일부러 예외를 발생시켜보았다. 이 프로그램을 실행하면, 다음과 같이 ArithmeticException이 발생한다.

divide()메서드를 호출하는 코드에서 에러가 발생했다. 그래서 그 다음 콘솔에 출력하는 코드가 출력되지 않았다. 그런데 컴파일할 때는 발생하지 않았는데, 프로그램을 실행할때 발생했다. 예외(Exception)이 발생했다는 건 에러가 발생한것과 다른 경우인데, 이에 대해선 뒤에서 다루었다. (바로가기)Exception과 Error의 차이는?

컴파일 에러면 개발하는 과정에서 개발자가 디버깅하기 쉽겠지만, 이런 경우는 개발하는 과정에서 개발자가 미리 예측하여 예외처리를 해주어야 한다.

ArithmeticException이 발생할 경우 예외처리를 하도록 try/catch를 적용해보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StudyHalle {
public static void main(String[] args){
StudyHalle study = new StudyHalle();
try {
System.out.println(study.divide(10,0));
} catch(ArithmeticException e){
System.out.println("Exception : "+e);
}
System.out.println("예외처리를 위한 예제 코드");
}
public float divide(int num1, int num2){
return num1/num2;
}
}

다시 컴파일하고 프로그램을 실행한 결과는 아래와 같다.

ArithmeticException이 발생할 경우, 콘솔에 메세지를 출력하도록 한 결과이다. 프로그램에서 에러처럼 발생된게 아니라 예외처리를 한 덕분에 프로그램이 중단되지 않고, 예외처리 코드 이후의 코드까지 정상적으로 출력되었다.

이처럼 예외상황이 발생했을때, 프로그램이 중단되지 않고 개발자가 의도한 대로 프로그램이 실행되도록 핸들링하는 것을 예외처리라 한다. 예외처리를 할 때 사용하는 try-catch를 더 자세히 알아보겠다.


try

try-catch문에서 try는 예외처리가 발생할 것으로 예상되는 코드를 포함시키는 영역이다.

try-catch의 흐름은 아래의 그림과 같다.

try문에서 Exception이 발생하지 않으면 try-catch문을 벗어나며, Exception이 발생하면 catch에서 핸들링하는 예외인지를 체크하여 예외처리가 있으면 그 로직으로 프로그램이 정상동작하며, 예외처리 코드가 없으면 예외로 인해 프로그램이 중단된다.

위의 예제코드에서 보면 divide()를 호출하는 코드에서 분모가 0으로 들어올경우 ArithmeticException이 예상되므로 divide()를 호출하는 코드를 try 문안에 넣었다.

try 문안에 try/catch문을 중첩해서 사용할수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StudyHalle {
public static void main(String[] args){
StudyHalle study = new StudyHalle();
try {
try{
System.out.println(study.divide(10,0));
} catch(RuntimeException e){
System.out.println("message : "+e);
}
} catch(ArithmeticException e){
System.out.println("Exception : "+e);
}
System.out.println("예외처리를 위한 예제 코드");
}
public float divide(int num1, int num2){
return num1/num2;
}
}

이렇게 해서 프로그램을 실행하면 다음과 같은 결과를 출력한다.

중첩 try-catch문에 의해서 예외처리가 되었음을 알 수 있다.


catch

try문안에서 발생할 수 있는 예외(Exception)를 예외처리하는 영역이다. 위의 코드를 다시 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StudyHalle {
public static void main(String[] args){
StudyHalle study = new StudyHalle();
try {
try{
System.out.println(study.divide(10,0));
} catch(RuntimeException e){
System.out.println("message : "+e);
}
} catch(ArithmeticException e){
System.out.println("Exception : "+e);
}
System.out.println("예외처리를 위한 예제 코드");
}
public float divide(int num1, int num2){
return num1/num2;
}
}

아래 예외계층구조를 통해 더 알아보겠지만, 예외라고 하는 Exception은 클래스이다. ArithmeticException 같은 예외는 모두 Exception의 하위 클래스들이다.

catch로 예외처리된 Exception은 하위 클래스까지 모두 예외처리가 되는데, 이 때문에 RuntimeException이 예외처리 되었다면, RuntimeException의 하위클래스인 ArithmeticException도 예외처리가 된걸로 간주한다. 따라서 아래의 catch문이 실행되지 않은것이다.

멀티캐치(multi-catch)

예외처리는 복수로 할 수도 있으며, 비트논리합 연산자(참고)를 이용할수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StudyHalle {
public static void main(String[] args){
StudyHalle study = new StudyHalle();
try {
throw new NullPointerException();
} catch(ArithmeticException e){
System.out.println("Exception : "+e);
} catch(NullPointerException | IllegalArgumentException e) {
System.out.println("Exception : "+e);
}
System.out.println("예외처리를 위한 예제 코드");
}
public float divide(int num1, int num2){
return num1/num2;
}
}

try문에서 throw 키워드로 NullPointerException을 발생시켜서 예외처리가 어떻게 되는지 확인하였다. throw 키워드는 아래에서 설명한다.

비트논리 연산자를 이용한 catch를 멀티캐치라고 하는데, 이는 JDK 1.7부터 추가된 기능이다.

1
2
3
catch(NullPointerException | IllegalArgumentException e) {
System.out.println("Exception : "+e);
}

출처 : Oracle Java Documentation - The catch Blocks


throw

try-catch가 발생할 수 있는 Exception을 예외처리하는 코드라면, throw는 고의로  Exception을 발생하는 키워드이다.

Exception을 던지는 문법은 아래와 같다.

1
2
RuntimeException e = new RuntimeException();
throw e;

위에서 Exception도 객체라고 설명했다. 예외를 발생시키기 위해서는 Exception을 인스턴스로 선언해서 throw 키워드와 함께 사용하면 Exception을 발생시킬 수 있다.

일반적으로는 저렇게 2줄로 쓰기보다 아래처럼 한 줄로 사용한다.

1
throw new RuntimeException();

예외를 던지는 예제 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StudyHalle {
public static void main(String[] args){
StudyHalle study = new StudyHalle();
try {
throw new NullPointerException();
} catch(NullPointerException e){
System.out.println("Exception : "+e);
}
System.out.println("예외처리를 위한 예제 코드");
}
public float divide(int num1, int num2){
return num1/num2;
}
}

try문에서 NullPointerException 을 던져서 캐치가 작동하도록 유도했다.


throws

throw가 예외를 고의로 발생하는 키워드였다면, throws는 메서드 내부에서 발생한 예외를 메서드 외부로 던지는 키워드이다.

사실 throws 키워드를 사용하지 않더라도 어차피 메서드를 호출하는 영역에서 Exception이 발생한다. 따라서 throws 를 사용한다 하더라도 달라질것은 없다. 그렇다면 왜 사용하는걸까?

앞서 예외처리라는건 코드에서 발생할 수 있는 Exception을 개발자가 예외처리하는 것이라고 하였는데, 코드만 보고 발생할 수 있는 Exception을 바로 예상할 수 있는건 쉬운일이 아니다. 그런데 메서드에서 throws 키워드를 사용하면, 발생할 수 있는 Exception을 명시함으로써 메서드 호출부에서 실수로 Exception을 처리하지 못하는 실수를 방지할 수 있다.

예제 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StudyHalle {
public static void main(String[] args) {
StudyHalle study = new StudyHalle();
try {
System.out.println(study.createException(10,0));
} catch (ArithmeticException e){
System.out.println("0");
}
}

public int createException(int num1, int num2) throws ArithmeticException {
return num1/num2;
}
}

주의할 것

try-catch에서 catch가 예외처리를 하여 프로그램이 중단되지 않도록 한다면, throws 키워드를 사용한다는 것은 예외처리를 하는 것이 아니라 예외를 던짐으로써 책임을 넘기는 일이 된다.

따라서 throws 키워드가 있는 메서드를 호출하는 영역에서는 try-catch를 통해서 예외를 처리해주어야 한다.


finally

예외 발생 여부와 상관없이 무조건 실행되는 코드이다. 예제 코드를 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StudyHalle {
public static void main(String[] args) {
StudyHalle study = new StudyHalle();
try {
study.throwException();
} catch (ArithmeticException e){
System.out.println("Fail by Exception");
} finally {
System.out.println("finally Exception.");
}
System.out.println("Bye, Exception!");
}

public void throwException() throws RuntimeException {
throw new RuntimeException();
}
}

throwException()는 리턴타입은 없고, RuntimeException을 던지는 메서드이다. 메인 메서드에서 throwException()를 호출함으로써 예외가 발생했지만, catch에서 처리하지 못하면서 RuntimeException에 의해 프로그램이 중단되었다.

프로그램이 비정상적으로 중단되는 바람에 try-catch 바깥의 코드는 실행되지 못했지만, finally 문안의 코드는 예외 발생 여부와 상관없이 정상적으로 출력되었다.

예외와 상관없이 무조건 실행되어야할 코드는 finally안에 작성할 필요가 있다.


예외 계층 구조

Exception은 Error와 함께 Throwable을 상속받는 하위 클래스이다.

Error와 Exception의 차이는 아래에서 정리했다. 바로가기

그리고 위의 이미지처럼 많은 예외 클래스가 Exception 하위클래스로 상속관계가 구성된다.

그리고 Exception은 컴파일 단계에서 체크하는지 여부에 따라 다시 Checked Exception과 Unchecked Exception으로 구분한다.

Checked Exception과 Unchecked Exception은 다음과 같다.

Checked Exception

컴파일시 발생하는 예외가 Checked Exception이다. Checked Exception은 예외처리하지 않으면 컴파일 과정에서 예외가 발생하기 때문에 IDE에서 예외처리를 강제한다.

자주 쓰이는 Checked Exception은 다음과 같다.

IOException

java.io 패키지의 File System 또는 Data Stream을 사용하거나 java.net 패키지의 네트워크 애플리케이션을 사용할때 발생한다.

1
2
3
4
5
try {
new FileReader(new File("/invalid/file/location"));
} catch (FileNotFoundException e) {
LOGGER.info("FileNotFoundException caught!");
}

ParseException

.parse()을 사용하여 주어진 문자열을 기반으로 객체를 만드는데 파싱에서 에러가 발생할 경우 ParseException이 발생한다.

1
2
3
4
5
try {
new SimpleDateFormat("MM, dd, yyyy").parse("invalid-date");
} catch (ParseException e) {
LOGGER.error("ParseException caught!");
}

InterruptedException

자바 스레드에서 join(), sleep() 또는 wait()을 호출할 때마다 대기(WAITING)상태 또는 TIMED_WAITING 상태로 전환된다.

게다가 자바 스레드는 interrupt()를 이용하여 다른 스레드를 뺏어서 호출할 수 있는데, 이 때 이미 대기(WAITING, TIME_WAITING)상태인 스레드를 중단시킬경우 InterruptedException이 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ChildThread extends Thread {
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
LOGGER.error("InterruptedException caught!");
}
}
}

public class MainThread {
public static void main(String[] args) throws InterruptedException {
ChildThread childThread = new ChildThread();
childThread.start();
childThread.interrupt();
}
}

출처 : Baeldung - Common Java Exceptions

Unchecked Exception

런타임시 발생하는 예외가 Unchecked Exception이다. 컴파일 과정에서 체크되지 않기 때문에 IDE에서 예외처리를 강제하지는 않지만, 런타임시 발생할 수 있으므로 이 역시도 예외처리를 해주어야한다.

아래의 Unchecked Exception들은 RuntimeException의 하위 클래스들이다.

NullPointerException

애플리케이션에서 null을 사용하려고 할 때, 메서드에서 던지는 Exception.

1
2
String strObj = null;
strObj.equals("DevAndy"); // throws NullPointerException

ArrayIndexOutOfBoundsException

배열의 인덱스를 잘못 참조할때 발생하는 Exception.

1
2
int[] nums = {1,2,3,4,5};
int num = nums[-1]; // throws ArrayIndexOutOfBoundsException

StringIndexOutOfBoundsException

문자열의 인덱스를 잘못 참조할때 발생하는 Exception.

1
2
String str = "Hello World.";
char charAtLengthIndex = str.charAt(str.length()); // throws StringIndexOutOfBoundsException

NumberFormatException

문자열로부터 Integer 객체를 파싱할때, 문자열의 형식이 숫자형식이 아닐 때 발생하는 Exception.

1
2
3
4
String rightNumberFormat = "10000";
String wrongNumberFormat = "100ABCD";
int passedParsedNumber = Integer.parseInt(rightNumberFormat);
int wrongParsedNumber = Integer.parseInt(wrongNumberFormat); // throws NumberFormatException

ArithmeticException

0을 나누려고 할 때 발생하는 Exception.

1
int result = 10/0;  // throws ArithmeticException

ClassCastException

인스턴스가 아닌 하위 클래스로 타입 캐스팅 하려고 할 때, 발생하는 Exception.

1
2
3
public class Animal {
String name = "animal";
}
1
2
3
public class Dog {
String name = "dog";
}
1
2
3
public class Cat {
String name = "cat";
}
1
2
3
4
5
6
7
8
9
public class Human {
public static void main(String[] args){
Animal animal = new Cat();
System.out.println(animal.name);

Dog dog = (Dog) animal; // throws ClassCastException
System.out.println(dog.name);
}
}

dog 인스턴스를 선언하는 코드를 보면, Animal 타입의 인스턴스를 그 하위 클래스 타입으로 형변환을 하는 과정에서 ClassCastException이 발생했다. 하위클래스로의 형변환은 불가능하기 때문이다.

출처 : Baeldung - Common Java Exceptions


Exception의 계층구조를 알아두어야 하는 이유가 있다.

catch문의 순서는 계층구조의 순서를 따라서 작성해야 한다. 계층구조에서 하위 계층을 먼저 catch하고, 그 다음 catch 순서부터 상위계층을 catch하는 순으로 작성해야한다.

1
2
3
4
5
6
7
8
9
10
public class Example {
public static void main(String[] args){
try {
// try
} catch(RuntimeException e) {

} catch(IllegalArgumentException e) { // compile error
}
}
}

출처 : 백기선님 유튜브 - [자바 라이브 스터디] 9주차 예외

위의 코드는 두번째 catch문에서 컴파일 에러가 발생한다. 왜 그럴까?

출처 : Java API

자바 API 문서를 통해 보면, IllegalArgumentExceptionRuntimeException의 하위 클래스임을 알 수 있다.

다시 위의 코드로 돌아가면, catch 순서를 주목해야한다. RuntimeException을 먼저 catch했기 때문에, 그럼 RuntimeException의 하위클래스까지 전부 catch가 된다. 그런 상황에서 이미 catch된 클래스의 하위 클래스를 예외처리하려고 하니, 컴파일러 입장에서는 무엇을 예외처리해야할지 알 수 없기 때문에 컴파일 에러가 발생하는 것이다.


Exception과 Error의 차이

Throwable 클래스를 상속받으며, 발생할 경우 프로그램이 중단된다는 공통점이 있음에도 Error와 Exception은 분명히 다른 클래스이다.

프로그램을 중단시키는 이벤트가 사전에 예측할 수 있었느냐, 없었느냐에 따라 Exception과 Error를 구분할 수 있다.

에러는 개발자가 예측할 수 없었던 이벤트로 인해 개발자가 복구할 수 없는 상황이다.

반면 예외는 개발자가 예측할 수 있으며, 예측을 하였다면 예외처리를 통해서 예외를 제어할수도 있다.

출처


RuntimeException과 RE가 아닌 것

RuntimeException이란 말그대로 컴파일 시점이 아닌 런타임 시점에서 발생하는 예외를 뜻한다.

RuntimeException컴파일 시점에 체크하지 않는 예외이기 때문에 Unchecked Exception에 해당하며, Unchecked Exception은 개발자가 유효성 검증을 통해서 회피할 수 있다.

Exception은 크게 Checked Exception과 Unchecked Exception으로 구분할 수 있다. 어떤 블로그 글에서 Checked Exception과 Unchecked Exception을 클래스처럼 표현했는데, Exception을 상속받는 하위 클래스들의 분류라고 보는것이 올바른것 같다.

정리하면, RuntimeException은 Unchecked Exception이며, RE가 아닌 예외는 Checked Exception이 된다.

Exception이 명확하지 않거나 Exception으로 인한 문제를 해결할 수 없다면 Unchecked Exception을, Exception이 발생하면 치명적이거나 Exception을 활용하여 의미있는 작업을 할 수 있다면 Checked Exception을 사용한다고 한다.

출처 : max9106 - [Java] Exception


커스텀한 예외 만드는 법

커스텀한 예외를 만드는 방법은 간단한다. Exception은 기본적으로 클래스기 때문에 Exception을 상속받는 클래스를 생성하면 된다.

계좌 잔액보다 큰 금액이 이체되려고 할 때 발생하는 BalanceInsuffientException을 생성했다.

1
2
3
4
5
6
public class BalanceInsuffientException extends Exception {
public BalanceInsuffientException(){}
public BalanceInsuffientException(String message){
super(message);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Account {
public static void main(String[] args) throws BalanceInsuffientException {
int 계좌 = 1_000_000;
int 출금액 = 1_200_000;
transferMoney(계좌,출금액);
}

public static void transferMoney(int acount, int transfer) throws BalanceInsuffientException {

if (acount<transfer){
throw new BalanceInsuffientException("잔고 부족!!");
}
System.out.println("정상적으로 이체 성공!!");
}
}


예외활용하는 방법

단순히 try-catch로 예외를 처리하는 것을 넘어 이 예외의 메세지를 콘솔에 출력하는 방식이다. 위의 화면에서 커스텀 예외가 발생하도록 유도했는데, 사실 저렇게만 예외가 발생하면 안된다.

try-catch로 예외를 처리해주고, 이때 발생하는 예외 객체를 활용하는 3가지 메서드를 소개한다.

getMessage()

예외 메세지를 가져오는 메서드이다. 위의 예제코드를 활용했다.

1
2
3
4
5
try {
transferMoney(계좌, 출금액);
} catch (BalanceInsuffientException e){
System.out.println(e.getMessage());
}
1
잔고 부족!!

toString()

1
2
3
4
5
try {
transferMoney(계좌, 출금액);
} catch (BalanceInsuffientException e){
System.out.println(e.toString());
}
1
exceptionhandling.BalanceInsuffientException: 잔고 부족!!

printStackTrace()

1
2
3
4
5
try {
transferMoney(계좌, 출금액);
} catch (BalanceInsuffientException e){
e.printStackTrace();
}
1
2
3
exceptionhandling.BalanceInsuffientException: 잔고 부족!!
at exceptionhandling.Account.transferMoney(Account.java:21)
at exceptionhandling.Account.main(Account.java:9)

출처 : 생활코딩 - 예외1 문법