예외 처리(exception handling)
프로그램 오류
프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우가 있는데, 이러한 결과를 초래하는 원인을 프로그램 오류/에러
라고한다.
발생 시점에 따라 구분되는 오류
구분 설명 컴파일 에러(compile-time error) 컴파일 할 때 발생하는 에러 런타임 에러(runtime error) 프로그램 실행 도중에 발생하는 에러 논리적 에러(logical error) 실행은 되지만, 의도와 다르게 동작하는 것 소스코드를 컴파일하면, 컴파일러가 소스코드(.java)에 대해 오타나 잘못된 구문, 자료형 체크 등 기본적인 검사를 수행하여 오류가 있는지 알려준다. 컴파일을 성공적으로 마치면 클래스 파일(.class)이 생성되고, 생성된 클래스 파일을 실행할 수 있게 되는 것이다.
자바에서는 런타임 에러를
에러(error)
와예외(exception)
두 가지로 구분하였다.- 에러
- 일단 발생하면 복구할 수 없는 심각한 오류 (메모리 부족(OutOfMemeryError), 스택오버플로우(StackOverflowError) 등)
- 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
- 비정상적인 종료가 된다.
- 예외
- 발생하더라도 수습될 수 있는 비교적 덜 심각한 것
- 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류
- 예외가 발생하더라도 프로그래머가 이에 대한 적절한 코드를 미리 작성해놓음으로써 비정상적인 종료를 막을 수 있다.
- 에러
예외 클래스의 계층 구조
자바에서는 실행 시 발생할 수 있는 오류를 클래스로 정의하였다. (Exception과 Error)
예외 클래스들은 두 그룹으로 나눠질 수 있다.
- Exception 클래스들 : 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외
- FileNotFoundException : 존재하지 않는 파일의 이름을 입력했을 때
- ClassNotFoundException : 실수로 클래스의 이름을 잘못 적었을 때
- DataFormatException : 입력한 데이터 형식이 잘못되었을 때
- RuntimeException 클래스들 : 프로그래머의 실수로 발생하는 예외
- ArrayIndexOutOfBoundsException : 배열의 범위를 벗어났을 때
- NullPointerException : 값이 null인 참조변수의 멤버를 호출했을 때
- ClassCastException : 클래스 간의 형변환을 잘못했을 때
- ArithmeticException : 정수를 0으로 나누려고 했을 때
예외처리하기
예외처리
- 정의 : 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것
- 목적 : 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것
발생한 예외를 처리하지 못하면,
- 프로그램이 비정상적으로 종료
- 처리하지 못한 예외(uncaught exception)은 JVM의 예외처리기(UncaughtExceptionHandler)가 받아서 예외의 원인을 화면에 출력한다.
try-catch문
구조
1
2
3
4
5
6
7
8
9
try {
// 예외가 발생할 가능성이 있는 문장들을 넣는다.
} catch (Exception1 e1) {
// Exception1이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch {Exception2 e2} {
// Exception2가 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch {Exception3 e3} {
// Exception3이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
}
하나의 try블럭과 하나 이상의 catch블럭
발생한 예외의 종류와 일치하는 단 한 개의 cat블럭만 수행된다. (발생한 예외의 종류와 일치하는 catch블럭이 없으면 예외는 처리되지 않는다.)
if문과 달리, try블럭 혹은 catch블럭 내에 포함된 문장이 하나뿐이어도 괄호
{}
를 생략할 수 없다.중첩되게 사용할 수 있다.
- catch블럭의 괄호 내에 선언된 변수는 catch블럭 내에서만 유효하므로, 모든 catch블럭에 참조변수 ‘e’ 하나만 사용해도 된다.
- 중첩된 catch블럭 내에서 같은 이름의 참조변수를 사용해서는 안된다. (두 참조변수의 영역이 서로 겹치므로 다른 이름을 사용해야 구별되기 때문이다.)
1 2 3 4 5
try { try {} catch (Exception e) {} } catch (Exception e) { try {} catch (Exception e) {} // 에러, 변수 e가 중복 선언 되었다. }
흐름
- try 블럭 내에서 예외가 발생한 경우
- 발생한 예외와 일치하는 catch블럭이 있는지 확인한다.
- 일치하는 catch블럭을 찾으면 그 catch블럭 내의 문장들을 수행하고 전체 try-catch문을 빠져나가서 그 다음 문장을 계속해서 수행한다. 만일 일치하는 catch블럭을 찾지 못하면, 예외는 처리되지 못한다.
- try 블럭 내에서 예외가 발생하지 않은 경우
- catch블럭을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속한다.
catch블럭
1
2
3
4
5
6
7
8
9
10
11
try {
System.out.println(1);
System.out.println(0/0); // 0으로 나눠서 ArithmeticException 발생
System.out.println(2); // 실행되지 않고 넘어간다.
} catch (ArithmeticException ae) {
System.out.println("ArithmeticException");
ae.printStackTrace();
System.out.println("예외메시지 : " + ae.getMessage());
} catch (Exception e) {
System.out.println("Exception");
}
Exception클래스
- 맨 마지막의 catch블럭에서 Exception 클래스 타입의 참조변수를 선언해 놓으면, 어떤 종류의 예외가 발생하더라도 이 catch블럭에 의해서 처리된다.
- 모든 예외 클래스는 Exception클래스의 자손이기 때문이다.
예외 정보 출력
printStackTrace()
: 예외발생 당시의 호출스택(call stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.getMessage()
: 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.
위 소스 출력 예시
1
2
3
4
1
java.lang.ArithmeticException: / by zero
at ExceptionEx8.main(ExceptionEx8.java:7)
예외메시지 : / by zero
멀티 catch블럭
|
기호로 여러 catch블럭을 합칠 수 있게 한다.
1
2
3
4
5
try {
// ...
} catch (ExceptionA | ExceptionB e) {
e.printStackTrace();
}
멀티 catch블럭으로 연결된 예외 클래스가 조상과 자손 관계에 있으면 컴파일 에러가 발생한다.
- 불필요한 코드는 제거하라는 의미에서 에러가 발생하는 것 (조상클래스만 써줄 것)
멀티 catch블럭 내에서는 실제로 어떤 예외가 발생한 것인지 알 수 없다.
1 2 3 4 5 6 7 8 9
try { // ... } catch (ExceptionA | ExceptionB e) { e.methodA(); // 에러. ExceptionA에 선언된 methodA()는 호출 불가 if (e instanceof ExceptionA) { ExceptionA e1 = (ExceptionA)e; e1.methodA(); // OK. ExceptionA에 선언된 메서드 호출 가능 } }
- 그래서 예외 클래스들의 공통 분모인 조상 예외 클래스에 선언된 멤버만 사용할 수 있다.
- 필요하다면
instanceof
로 어떤 예외가 발생한 것인지 확인하고 개별적으로 처리할 수 있다. (이렇게까지 해가면서 블럭을 합치는 일은 거의 없을 것이다.)
멀티 catch블럭에 선언된 참조변수 e는 상수이므로 값을 변경할 수 없다.
- 여러 catch블럭이 하나의 참조변수를 공유하기 때문에 생기는 제약이다. (실제로 참조변수의 값을 바꿀 일은 없을 것이다.)
예외 발생시키기
방법
- 먼저, 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든 다음
- 키워드
throw
를 이용해서 예외를 발생시킨다.
1
2
3
4
5
6
7
8
9
10
11
12
class ExceptionEx {
public static void main(String args[]) {
try {
Exception e = new Exception("고의로 발생시킴");
throw e; // 예외를 발생시킴
//throw new Exception("고의로 발생시킴"); // 위의 두줄을 한줄로 줄일 수 있다.
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("정상 종료");
}
}
실행 결과
1
2
3
java.lang.Exception: 고의로 발생시킴
at ExceptionEx.main(ExceptionEx.java:4)
정상 종료
checked 예외, unchecked 예외
checked 예외
- Exception 클래스들이 발생할 가능성이 있는 문장에 예외처리를 하지 않으면 컴파일 되지 않는다.
- 예외처리를 확인하는 Exception 클래스들은 checked예외라고 한다.
unchecked 예외
- RuntimeException 클래스들이 발생할 가능성이 있는 문장에 예외처리를 하지 않으면 컴파일은 성공하지만, 비정상적으로 종료된다.
- 예외처리를 확인하지 않는 RuntimeException 클래스들은 unchecked예외라고 한다.
메서드에 예외 선언하기
방법
- 메서드 선언부에 키워드
throws
를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어준다. - 예외가 여러 개일 경우 쉼표
,
로 구분한다.
1
2
3
void method() throws Exception1, Exception2, ... ExceptionN {
// 메서드 내용
}
적어준 예외 뿐만 아니라 그 자손타입의 예외까지도 발생할 수 있다.
메서드 선언부에 발생 가능성 있는 예외를 명시하여 이 메서드를 사용하는 쪽에서 이에 대한 처리를 강요하도록 한다.
- 사용하는 쪽에서 예외처리를 하지 않고 종료된다면 예외로 인해 비정상적으로 종료된다.
- 이를 통해 프로그래머들의 짐을 덜어주고, 견고한 프로그램 코드를 작성할 수 있도록 도와준다.
예외가 발생한 메서드 내에서 자체적으로 처리해도 되는 것은 메서드 내에서 try-catch문을 사용해서 처리하고, 메서드 내에서 자체적으로 해결이 안되는 경우에는 예외를 메서드에 선언해서 호출한 메서드에서 처리해야한다.
자체적으로 처리
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class ExceptionEx { public static void main(String[] args) { method1(); } static void method1() { try { throw new Exception(); } catch (Exception e) { System.out.prinln("method1에서 예외처리"); e.printStackTrace(); } } }
호출한 메서드에서 처리
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class ExceptionEx { public static void main(String[] args) { try { method1(); } catch (Exception e) { System.out.prinln("main에서 예외처리"); e.printStackTrace(); } } static void method1() throws Exception{ throw new Exception(); } }
finally 블럭
- 목적 : 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시키기 위해 사용한다.
- 사용법 : try-catch문 끝에 선택적으로 덧붙인다.
- 실행 순서 : try → catch → finally (예외가 발생하지 않은 경우 catch 생략)
1
2
3
4
5
6
7
try {
// 예외가 발생할 가능성이 있는 문장들을 넣는다.
} catch (Exception1 e) {
// 예외처리를 위한 문장을 적는다.
} finally {
// 예외의 발생여부에 관계없이 항상 수행되어야하는 문장들을 넣는다.
}
- try, catch블럭 내에 return문이 있더라도 finally블럭 내 문장들을 실행하고 종료한다.
자동 자원 반환 (try-with-resources문)
JDK1.7부터 사용 가능
주로 입출력 관련 클래스에서 사용
입출력에 사용되는 클래스는 사용 후 자원(resources)를 반환하기 위해 닫아줘야한다.
1 2 3 4 5 6 7 8 9 10 11 12 13
try { fis = new FileInputStream("score.dat"); dis = new DataInputStream(fis); } catch (IOException ie) { ie.printStackTrace() } finally { try { // close()에서 발생할 수 있는 예외를 처리하기 위해 if(dis != null) dis.close(); } catch (IOException ie) { ie.printStackTrace() } }
- 코드가 복잡해졌다.
- try블럭과 finally블럭에서 모두 예외가 발생하면, try블럭의 예외는 무시된다.
이를 개선하기 위해 try-with-resources문이 되었다.
사용방법
1
2
3
4
5
6
try (FileInputStream fis = new FileInputStream("score.dat");
DataInputStream dis = new DataInputStream()) {
// 코드 작성..
} catch (IOException ie) {
ie.printStackTrace();
}
- 괄호
()
안에 객체를 생성하는 문장을 넣으면, try블럭을 벗어나는 순간 자동적으로 close()가 호출된다.- 생성된 객체는 try블럭 내에서만 사용 가능하다.
- 자동으로 close()가 호출될 수 있으려면 클래스가
AutoCloseable
이라는 인터페이스를 구현한 것이어야 한다.
- 괄호
()
안에 두 문장 이상 넣을 경우;
으로 구분한다.
억제된 예외 (suppressed)
try-with-resources문을 사용하면 try-catch만 사용했을 때 try블럭과 catch블럭에서 모두 예외가 발생할 경우 try블럭에서 발생해서 무시되었을 예외를 출력할 수 있다.
이 때 출력된 예외를 억제된 예외
라고 한다.
억제된 예외에 대한 정보는 실제 발생한 예외에 저장된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class TryWithResourcesEx {
public static void main(String[] args) {
try (CloseableResource cr = new CloseableResource) {
cr.exceptionWork(false); // 예외가 발생하지 않는다.
} catch (WorkException e) {
e.printStackTrace();
} catch (CloseEXception e) {
e.printStackTrace();
}
try (CloseableResource cr = new CloseableResource) {
cr.exceptionWork(true); // 예외가 발생한다.
} catch (WorkException e) {
e.printStackTrace();
} catch (CloseEXception e) {
e.printStackTrace();
}
}
}
class CloseableResource implements AutoCloseable {
public void exceptionWork(boolean exception) throws WorkException {
System.out.println("exceptionWork(" + exception + ")가 호출됨");
if(exception)
throw new WorkException("WorkException 발생");
}
public void close() throws CloseException {
System.out.println("close() 호출됨");
throw new CloseException("CloseException 발생");
}
}
class WorkException extends Exception {
WorkException(String msg) { super(msg); }
}
class CloseException extends Exception {
CloseException(String msg) { super(msg); }
}
실행결과
1
2
3
4
5
6
7
8
9
10
11
12
13
14
exceptionWork(false)가 호출됨
close() 호출됨
CloseException: CloseException 발생
at CloseableResource.close(TryWithResourcesEx.java:33)
at TryWithResourcesEx.main(TryWithResourcesEx.java:6)
exceptionWork(true)가 호출됨
close() 호출됨
WorkException: WorkException 발생
at CloseableResource.exceptionWork(TryWithResourcesEx.java:28)
at TryWithResourcesEx.main(TryWithResourcesEx.java:14)
CloseException: CloseException 발생
at CloseableResource.close(TryWithResourcesEx.java:33)
at TryWithResourcesEx.main(TryWithResourcesEx.java:15)
- 두 예외가 동시에 발생할 수는 없기 때문에 실제 발생된 예외는 WorkException, 억제된 예외는 CloseException으로 다룬다.
Throwable에 정의된 억제된 예외와 관련된 메서드
1
2
void addSuppressed(Throwable exception) 억제된 예외를 추가
Throwable[] getSuppressed() 억제된 예외(배열)을 반환
사용자정의 예외
- 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다.
- 가능하면 새로운 예외클래스를 만들기보다 기존의 예외클래스를 사용하자.
방법
Exception
클래스 또는 RuntimeException
으로부터 상속받아 클래스를 만든다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyException extends Exception {
// 에러코드 값을 저장하기 위한 필드를 추가했다.
pricate final int ERR_CODE; // 생성자를 통해 초기화한다.
MyException(String msg, int errCode) { // 생성자
super(msg); // 조상인 Exception클래스의 생성자를 호출한다.
ERR_CODE = errCode;
}
MyException(String msg){ // 생성자
this(msg, 100); // ERR_CODE를 100(기본값)으로 초기화한다.
}
public int getErrCode(){ // 에러 코드를 얻을 수 있는 메서드도 추가한다.
return ERR_CODE; // 이 메서드는 주로 getMessage()와 함께 사용될 것이다.
}
}
예외 되던지기 (exception re-throwing)
예외를 처리한 후 인위적으로 다시 발생시키는 것
하나의 예외에 대해서 예외가 발생한 메서드와 이를 호출한 메서드 양쪽 모두에서 처리해줘야 할 작업이 있을 때 사용된다.
방법
- 예외가 발생할 가능성이 있는 메서드에서 try-catch문을 사용해서 예외 처리하기
- catch문에서 필요한 작업을 수행한 후 throw문을 사용해서 예외를 다시 발생시키기 (+메서드의 선언부에 발생할 예외를 throws에 지정해주어야 한다.)
- 다시 발생한 예외는 이 메서드를 호출한 메서드에게 전달되어 호출한 메서드의 try-catch문에서 예외를 또다시 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class RethrowingEx {
public static void main(String[] args) {
try {
method1();
} catch (Exception e) {
System.out.println("main메서드에서 예외가 처리되었습니다.");
}
}
public void method1() throws Exception {
try {
throw new Exception();
} catch (Exception e) {
System.out.println("method1메서드에서 예외가 처리되었습니다.");
throw e; // 다시 예외를 발생시킨다.
}
}
}
실행결과
1
2
method1메서드에서 예외가 처리되었습니다.
main메서드에서 예외가 처리되었습니다.
반환값이 있는 메서드일 경우
1 2 3 4 5 6 7 8 9 10
static int method() { try { return 0; } catch (Exception e) { // return 1; throw new Exception(); } finally { System.out.println("method1메서드의 finally블럭이 수행되었습니다."); } }
- catch블럭에서도 return문이 있어야 한다.
- 또는 예외 되던지기를 해서 호출한 메서드로 예외를 전달하면 return문이 없어도 된다.
- finally블럭에서도 return문을 사용할 수 있으며, try블럭이나 catch블럭 return문 다음에 수행된다. 최종적으로 finally블럭 내의 return문 값이 반환된다.
- catch블럭에서도 return문이 있어야 한다.
연결된 예외(chained exception)
예외 A가 예외 B를 발생시켰다면, A를 B의 원인 예외(cause exception)이라고 한다.
1
2
Throwable initCause(Throwable cause) 지정한 예외를 원인 예외로 등록
Throwable getCause() 원인 예외를 반환
SpaceException을 원인 예외로 하는 InstallException을 발생시키는 방법
1
2
3
4
5
6
7
8
9
10
try {
startInstall(); // SpaceException 발생
copyFiles();
} catch (SpaceException e) {
InstallException ie = new InstallException("설치 중 예외 발생"); // 예외 생성
ie.initCause(e); // InstallException의 원인 예외를 SpaceException으로 지정
throw ie; // InstallException을 발생시킨다.
} catch (MemoryException me) {
// ...
}
발생한 예외를 원인 예외로 등록해서 다시 예외를 발생시키는 이유
여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서
InstallException을 SpaceException과 MemeryException의 조상으로 해서 catch블럭을 작성하면,
1 2 3 4 5 6
try { startInstall(); // SpaceException 발생 copyFiles(); } catch (InstallException e) { e.printStackTrace(); }
- 실제 발생한 예외가 알 수 없다는 문제가 생긴다.
- SpaceException과 MemoryException의 상속관계를 변경해야되는 것이 부담이 된다.
→ 예외가 원인 예외를 포함할 수 있게 하면 두 예외는 상속관계가 아니어도 상관없다.
checked예외를 unchecked예외로 바꿀 수 있도록 하기 위해서
checked예외를 unchecked예외로 바꾸면 예외처리가 선택적이 되므로 억지로 예외처리를 하지 않아도 된다.
1 2 3 4 5 6 7 8 9
static void startInstall() throws SpaceException { if(!enoughSpace()) { throw new SpaceException("설치할 공간이 부족합니다."); } if(!enoughMemory()) { throw new RuntimeException(new MemoryException("메모리가 부족합니다.")); } }
MemoryException은 Exception의 자손이어서 반드시 예외처리를 해야되는데, 이 예외를 RuntimeException으로 감싸서 unchecked예외로 바꿨다.
그래서 throws에 MemoryException을 선언하지 않아도 된다.
initCause() 대신 RuntimeException 생성자 사용
1
RuntimeException(Throwable cause) 원인 예외를 등록하는 생성자
출처📎
- 자바의 정석