https://github.com/whiteship/live-study
백기선님 자바 기초 스터디 9주차
목표
자바의 예외 처리에 대해 학습하세요.
학습할 것 (필수)
- 자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
- 자바가 제공하는 예외 계층 구조
- Exception과 Error의 차이는?
- RuntimeException과 RE가 아닌 것의 차이는?
- 커스텀한 예외 만드는 방법
자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
프로그램 실행도중에 발생하는 예외는 프로그래머가 이에 대한 처리를 미리 해주어야 한다.
예외 처리(exception handling)
- 정의 - 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것
- 목적 - 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것
발생한 예외를 처리하지 못하면, 프로그램은 비정상적으로 종료되며, 처리되지 못한 예외(uncaught exception)는 JVM의 '예외 처리기(UncaughtExceptionHandler)'가 받아서 예외의 원인을 화면에 출력한다.
예외 처리를 하기 위해서는 앞으로 소개할 다양한 방식을 사용할 수 있다. 하나하나 살펴보자.
try, catch
예외 처리를 위한 try-catch문의 구조는 다음과 같다.
try {
//예외가 발생할 가능성이 있는 문장들을 넣는다.
} catch (Exception1 e1) {
//Exception1이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch (Exception2 e2) {
//Exception2이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch (ExceptionN eN) {
//ExceptionN이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
}
하나의 try블럭 다음에는 여러 종류의 예외를 처리할 수 있도록 하나 이상의 catch블럭이 올 수 있으며,
이 중 발생한 예외의 종류와 일치하는 단 한 개의 catch블럭만 수행된다.
발생한 예외의 종류와 일치하는 catch블럭이 없으면 예외는 처리되지 않는다.
catch블럭 괄호()내에는 처리하고자 하는 예외와 참조변수 하나를 선언해야 한다.
예외가 발생하면, 발생한 예외에 해당하는 클래스의 인스턴스가 만들어진다.
다음 예제를 보자.
package week9;
public class TryCatch {
public static void main(String[] args) {
System.out.println(1);
System.out.println(2);
try {
System.out.println(3);
System.out.println(0 / 0); //예외발생!!!
System.out.println(4); //실행되지 않는다.
} catch (ArithmeticException ae) {
ae.printStackTrace();
System.out.println("예외메시지 : " + ae.getMessage());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(5);
}
}
<결과>
1
2
3
java.lang.ArithmeticException: / by zero
at week9.TryCatch.main(TryCatch.java:10)
예외메시지 : / by zero
5
예제를 보면 try블럭 안에서 정수(0)를 0으로 나누려 했기 때문에 'ArithmeticException'이 발생했고 이를 catch블럭에서 처리하는 과정이다.
<Point 1>
여기서 중요한 것은 try블럭 내에서 예외가 발생하면 그 즉시 catch문으로 이동하고,
그 다음 문장(System.out.println(4);)는 실행되지 않는다는 점이다.
<Point 2>
try블럭 내에서 예외가 발생하면 catch블럭 괄호()내의 예외클래스의 인스턴스에 대해 instanceof연산자를 이용해서 차례대로 catch블럭을 검사하게 된다.
예제의 경우 try블럭 내에서 'ArithmeticException'이 발생하고 이는 두 catch블럭, 'ArithmeticException ae', 'Exception e' 인스턴스 모두 instanceof 결과 true가 된다.
(Exception은 모든 예외 클래스의 공통 조상이다)
그러나 실제로는 첫 번째 catch블럭인 'ArithmeticException ae'에서 instanceof 검사에 일치하기 때문에 두 번째 catch문을 검사하지 않고 넘어가게 된다.
만일 try블럭 내에서 ArithmeticException이 아닌 다른 종류의 예외가 발생한 경우에는 두 번째 catch블럭인 Exception클래스 타입의 참조변수를 선언한 곳에서 처리되었을 것이다.
-> 그래서 하나의 try블럭에 대해 여러 개의 catch블럭을 사용할 때는, Exception의 순서가 중요하다.
<Point3>
예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨 있으며, getMessage(),와 printStackTrace() 등 예외 인스턴스의 메서드를 통해서 이 정보들을 얻을 수 있다.
- printStackTrace(): 예외발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.
- getMessage(): 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.
예제 결과를 보면 printStackTrace()를 사용해서 호출스택(Call Stack)에 대한 정보와 getMessage()를 통해서 예외 메시지를 출력한 것을 확인할 수 있다.
<Point4 - 멀티 catch블럭>
JDK 1.7부터 여러 catch블럭을 '|' 기호를 이용해서, 하나의 catch블럭으로 합칠 수 있게 되었으며, 이를 '멀티 catch블럭'이라 한다.
멀티 catch블럭을 이용하면 중복된 코드를 줄일 수 있다.
try {
...
} catch (ExceptionA e) {
e.printStackTrace();
} catch (ExceptionB e2) {
e2.printStackTrace();
}
-> 멀티 catch블럭
try {
...
} catch (ExceptionA | ExceptionB e) {
e.printStackTrace();
}
여기서 주의할 점은, 만약 멀티 catch블럭의 연결된 예외 클래스가 서로 상속 관계에 있다면 컴파일 에러가 발생한다.
try {
...
} catch (ParentException | ChildException e) { //에러!
e.printStackTrace();
}
왜냐하면, 두 예외 클래스가 상속 관계에 있다면, 그냥 부모 클래스만 써주는 것과 똑같기 때문이다.
불필요한 코드는 제거하라는 의미에서 에러가 발생하는 것이다.
그리고 멀티 catch블럭에서 참조변수는 실제 발생한 예외와 매칭이 되어 동작하지만,
catch블럭에 연결된 예외 클래스들의 공통 분모인 부모 예외 클래스에 선언된 멤버들만 사용할 수 있다.
다음 예제를 보자.
try {
...
} catch (ExceptionA | ExceptionB e) {
e.methodA(); //에러. ExceptionA에 선언된 methodA()는 호출불가
if (e instanceof ExceptionA) {
ExceptionA eA = (ExceptionA) e;
eA.methodA(); //OK. ExceptionA에 선언된 메서드 호출가능
} else {
...
}
e.printStackTrace();
}
만약 try블럭에서 ExceptionA가 발생했어도 ExceptionA에 멤버인 methodA()는 호출할 수 없게 된다.
필요하다면, 위와 같이 instanceof연산자로 어떤 예외가 발생한 것인지 확인하고 개별적으로 처리할 수는 있지만,
차라리 따로 catch블럭을 생성하지 이렇게까지 할 일은 거의 없을 것이다.
다만, e.printStackTrace()처럼 두 예외의 공통 부모에 속한 멤버는 사용할 수 있다.
throw
throw 키워드는 프로그래머가 고의로 예외를 발생시킬 수 있게 한다.
public class Throw {
public static void main(String[] args) {
try {
throw new Exception("고의로 발생시켰음.");
} catch (Exception e) {
System.out.println("에러 메시지 : " + e.getMessage());
}
}
}
<결과>
에러 메시지 : 고의로 발생시켰음.
Exception인스턴스를 생성하고 이를 throw하면 예외를 고의로 발생시킬 수 있다.
+) 예외를 생성할 때 생성자에 String을 넣어 주면, 이 String이 Exception인스턴스에 메시지로 저장된다. 이 메시지는 getMessage()를 이용해서 얻을 수 있다.
throws
try-catch문을 사용하는 것 외에, 예외를 메서드에 선언해 처리하는 방법이 있다.
throws 키워드를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 된다.
void method() throws Exception1, Exception2, ... ExceptionN {
//메서드의 내용
}
자바에서는 이처럼 메서드를 작성할 때 메서드 내에서 발생할 가능성이 있는 예외를 메서드의 선언부에 명시하여 이에 대한 처리를 하도록 강요하기 때문에, 어떤 상황에 어떤 예외가 발생할지 try-catch를 통해 예측해야 하는 프로그래머들의 짐을 덜어주는 것을 물론 보다 견고한 프로그램 코드를 작성할 수 있도록 도와준다.
사실 예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라, 자신(예외가 발생한 메서드)을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다.
예외를 전달 받은 메서드가 또다시 자신을 호출한 메서드에세 전달하며, 이런 식으로 계속 호출스택에 있는 메서드들을 따라 전달되다가 제일 마지막에 있는 main메서드에서도 예외가 처리되지 않으면, main메서드 마저 종료되어 예외로 인해 프로그램이 전체가 종료된다.
결국 예외가 발생한 메서드 혹은 이를 호출한 메서드 어느 한 곳에서는 try-catch문으로 예외처리를 해주어야 한다.
다음 예제를 보자.
public class Throws {
public static void main(String[] args) {
method1();
}
static void method1() {
try {
throw new Exception();
} catch (Exception e) {
System.out.println("method1 메서드에서 예외가 처리되었습니다.");
e.printStackTrace();
}
}
}
<결과>
method1 메서드에서 예외가 처리되었습니다.
java.lang.Exception
at week9.Throws.method1(Throws.java:11)
at week9.Throws.main(Throws.java:6)
예제는 throws 없이 예외가 발생한 메서드(method1)에서 try-catch로 예외를 처리하고 있다.
이번에는 throws를 사용해 예외처리를 호출한 메서드로 전달해보자.
public class Throws {
public static void main(String[] args) {
try {
method1();
} catch (Exception e) {
System.out.println("main메서드에서 예외가 처리되었습니다.");
e.printStackTrace();
}
}
static void method1() throws Exception{
throw new Exception();
}
}
<결과>
main메서드에서 예외가 처리되었습니다.
java.lang.Exception
at week9.Throws.method1(Throws.java:15)
at week9.Throws.main(Throws.java:7)
두 예제 모두 main메서드가 method1()을 호출하며, method1()에서 예외가 발생한다.
차이점은 예외처리 방법에 있다.
두 번째 예제는 예외가 발생한 method1()에서 예외를 처리하는 것이 아닌 throws 키워드로 자신을 호출한 메서드로 예외 처리를 떠넘긴다. 그래서 main메서드에서 try-catch로 예외처리를 했다.
이처럼 예외가 발생한 메서드 method1()에서 예외를 처리할 수도 있고, 예외가 발생한 메서드를 호출한 메서드 main메서드에서 처리할 수도 있다.
finally
finally블럭은 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용된다.
try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있다.
public static void main(String[] args) {
try {
//예외가 발생할 가능성이 있는 문장들을 넣는다.
} catch (Exception1 e1) {
//예외처리를 위한 문장을 적는다.
} finally {
//예외의 발생여부에 관계없이 항상 수행되어야 하는 문장들을 적는다.
//finally블럭은 try-catch문의 맨 마지막에 위치해야 한다.
}
}
예외가 발생한 경우에는 'try -> catch -> finally'의 순으로 실행되고,
예외가 발생하지 않은 경우에는 'try -> finally'의 순으로 실행된다.
public static void main(String[] args) {
method1();
System.out.println("method1()의 수행을 마치고 main메서드로 돌아왔습니다.");
}
static void method1() {
try {
System.out.println("method1()이 호출되었습니다.");
return;
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("method1()의 finally블럭이 실행되었습니다.");
}
}
<결과>
method1()이 호출되었습니다.
method1()의 finally블럭이 실행되었습니다.
method1()의 수행을 마치고 main메서드로 돌아왔습니다.
위 결과에서 알 수 있듯이, try블럭에서 return문이 실행되는 경우에도 finally블럭의 문장들이 먼저 실행된 후에, 현재 실행 중인 메서드를 종료한다.
이와 마찬가지로 catch블럭의 문장 수행 중에 return문을 만나도 finally블럭의 문장들은 수행된다.
try-with-resuources
JDK 1.7부터 try-with-resources문이라는 try-catch문의 변형이 새로 추가되었다.
주로 입출력에 사용되는 클래스 중에서는 꼭 닫아줘야 하는 것들이 있다. 그래야 사용했던 자원(resources)이 반환되기 때문이다.
FileInputStream fis = null;
DataInputStream dis = null;
try {
fis = new FileInputStream("score.dat");
dis = new DataInputStream(fis);
} catch (IOException ie) {
ie.printStackTrace();
} finally {
try {
if (dis != null) {
dis.close();
}
} catch (IOException ie) {
ie.printStackTrace();
}
}
이렇게 DataInputStream으로 데이터를 읽는 도중에 예외가 발생하더라도 close될 수 있도록 finally블럭 안에 close()를 넣었다.
단, close() 역시 예외를 발생시킬 수 있기 때문에 finally블럭 안에 try-catch문을 추가했다.
이렇게 되면 코드가 복잡해져서 별로 보기에 좋지 않다. 이러한 점을 개선하기 위해 try-with-resources문을 사용한다.
try(FileInputStream fis = new FileInputStream("score.dat");
DataInputStream dis = new DataInputStream(fis)) {
} catch (IOException ie) {
ie.printStackTrace();
}
try문 괄호() 안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()를 호출하지 않아도 try블럭을 벗어나는 순간 자동적으로 close()가 호출된다. 그 다음에 catch블럭 또는 finally블럭이 호출된다.
자바가 제공하는 예외 계층 구조
자바는 실행 시 발생할 수 있는 예외(Exception)을 클래스로 정의하였다.
모든 예외의 최고 조상은 Exception클래스이며, 상속계층도를 Exception클래스로부터 도식화하면 다음과 같다.
Exception과 Error의 차이는?
자바에서는 실행 시(runtime) 발생할 수 있는 프로그램 오류를 '에러(error)'와 '예외(exception)', 두 가지로 구분하였다.
에러는 메모리 부족(OutOfMemoryError)이나 스택오버플로우(StackOverflowError)와 같이 일단 발생하면 복구할 수 없는 심각한 오류이고,
예외는 발생하더라도 수습될수 있는 비교적 덜 심각한 것이다.
에러가 발생하면, 프로그램의 비정상적인 종료를 막을 길이 없지만, 예외는 발생하더라도 프로그래머가 이에 대한 적절한 코드를 미리 작성해 놓음으로써 프로그램의 비정상적인 종료를 막을 수 있다.
자바에서는 예외(Exception)뿐만이 아닌 에러(Error) 역시 클래스로 정의하였다. 모든 클래스의 조상은 Object클래스이므로 Exception과 Error클래스 역시 Object클래스의 자손들이다.
Throwable클래스를 앞서 설명한 예외(Excpetion)클래스 계층 구조와 에러(Error)클래스가 상속하는 구조이다.
RuntimeException과 RE가 아닌 것의 차이는?
앞서 설명한 예외(Exception) 계층 구조를 다시 한 번 살펴보자.
위 그림에서 볼 수 있듯이 예외 클래스들은 다음과 같이 두 그룹으로 나눠질 수 있다.
- Exception클래스와 그 자손들 (그림의 윗부분, RuntimeException과 자손들 제외)
- RuntimeException클래스와 그 자손들(그림 아랫부분)
Exception클래스(+그 자손들)는 주로 외부의 영향으로 발생할 수 있는 것들로서, 프로그램 사용자들의 동작에 의해서 발생하는 경우가 많다. 예를 들면, 존재하지 않는 파일의 이름을 입력했다던가(FileNotFoundException), 실수로 클래스의 이름을 잘못 적는(ClassNotFoundException) 경우에 발생한다.
RuntimeException클래스(+그 자손들) 주로 프로그래머의 실수에 의해서 발생될 수 있는 예외들로 자바의 프로그래밍 요소들과 관계가 깊다. 예를 들면, 배열의 범위를 벗어난다던가(ArrayIndexOutOfBoundsException), 값이 null인 참조변수의 멤버를 호출하려는(NullPointException) 경우에 발생한다.
두 그룹은 예외 처리 관련해서도 차이점이 있다.
<Exception>
public static void main(String[] args) {
throw new Exception();
}
<컴파일 결과>
java: unreported exception java.lang.Exception; must be caught or declared to be thrown
위 에러는 예외처리가 되어야 할 부분에 예외처리가 되어 있지 않는다는 점이다.
만약 Exception클래스(+그 자손들)가 발생했을 때 따로 예외 처리(try-cath 등)를 해주지 않으면 컴파일 에러가 발생한다.
<RuntimeException>
public static void main(String[] args) {
throw new RuntimeException();
}
<실행 결과>
Exception in thread "main" java.lang.RuntimeException
at week9.Throw.main(Throw.java:6)
이 예제는 예외처리를 하지 않았음에도 불구하고 이전의 예제와는 달리 성공적으로 컴파일 된다.
RuntimeException클래스(+그 자손들)은 프로그래머에 의해 실수로 발생하는 것들이기 때문에 예외처리를 강제하지 않기 때문이다. 만일 RuntimeException클래스의 예외처리를 해야 한다면, try-catch문 등을 사용해서 예외처리를 따로 해주며 된다.
이처럼 컴파일러가 예외처리를 확인하지 않는 RuntimeException클래스와 그 자손들을 'unchecked예외'라고 부르고, 예외처리를 확인하는 Exception클래스와 그 자손들을 'checked예외'라고 부른다.
커스텀한 예외 만드는 방법
기존에 정의된 예외 클래스 외에 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다.
보통 Exception 혹은 RuntimeException클래스로부터 상속받아서 사용자 정의 예외를 생성한다.
public class MyException extends Exception {
//에러 코드 값을 저장하기 위한 필드를 추가했다.
private final int ERROR_CODE; //생성자를 통해 초기화 한다.
MyException(String message, int errorCode) {
super(message);
ERROR_CODE = errorCode;
}
MyException(String message) {
this(message, 400); //ERROR_CODE를 400(기본값)으로 초기화한다.
}
public MyException(String message, Throwable cause, int errorCode) {
super(message, cause);
ERROR_CODE = errorCode;
}
public int getErrorCode() {
return ERROR_CODE;
}
}
예제는 Exception클래스로부터 상속받아서 MyException클래스를 만들었다.
예제처럼 필요에 따라 변수나 메서드를 추가할 수 있다.
Exception클래스는 생성 시에 String 값을 받아 메시지로 저장할 수 있다. 따라서 MyException에도 메시지를 매개변수로 받는 생성자를 추가해주었다.
또한 ERROR_CODE 멤버변수를 추가해 에러코드 값도 저장할 수 있도록하였다.
이렇게 함으로써 MyException이 발생했을 때, catch블럭에서 getMessage()와 getErrorCode()를 사용해서 에러코드와 메시지를 모두 얻을 수 있을 것이다.
+) 추가
커스텀한 예외에서의 best practice 중 하나로 커스텀한 예외의 근본 원인을 생성자에 설정하는 것이 좋다.
무슨 말이냐면,
try {
throw new IOException();
} catch (IOException e) {
throw new MyException("IOException 발생", e, 500);
}
이런 코드가 있다고 하자.
IOException이 발생함으로써 MyException이 발생할 때, 즉 MyException의 원인이 IOException이라면,
MyException의 생성자에 원인 예외를 추가해줘야 이후에 원인을 추적할 때 많은 도움이 된다.
원인(cause)를 인자로 갖는 생성자는 MyException코드에 추가해 두었다.
'java > java' 카테고리의 다른 글
[Java] Enum (1) | 2021.08.27 |
---|---|
[Java] 멀티쓰레드 프로그래밍 (1) | 2021.08.23 |
[Java] 인터페이스 (2) | 2021.07.27 |
[Java] 패키지 (2) | 2021.07.21 |
[Java] 상속 (3) | 2021.07.12 |