Java의 정석 의 책을 읽고 정리한 내용입니다.
- 프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우가 있다.
- 이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다.
컴파일 에러 : 컴파일 시에 발생하는 에러
런타임 에러 : 실행 시에 발생하는 에러
논리적 에러 : 실행은 되지만, 의도와 다르게 동작하는 것
에러 (error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
예외 (exception) : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류
Exception클래스와 RuntimeException클래스 중심의 상속계층도
(1) Exception 클래스와 그 자손들 (그림 윗부분, RuntimeException과 자손들 제외)
(2) RuntimeException 클래스와 그 자손들 (그림 아랫 부분)
RuntimeException
클래스와 그 자손 클래스들 : RuntimeException
클래스들RuntimeException
클래스들을 제외한 나머지 클래스 : Exception
클래스들
Exception 클래스들 : 사용장 실수와 같은 외적인 요인에 의해 발생하는 예외
RuntimeException 클래스들 : 프로그래머의 실수로 발생하는 예외
🔔 예외 처리(exception handling)란?
프로그램 실행 시 발생할 수 있는 예기치 못한 예외의 발생에 대비한 코드를 작성하는 것이며, 예외처리의 목적은 예외의 발생으로 인한 실행 중인 프로그램의 갑작스런 비정상 종료를 막고, 정상적인 실행 상태를 유지할 수 있도록 하는 것이다.
예외처리 (exception handling) 의
정의 : 프로그램 실행시 발생할 수 있는 예외의 발생에 대비한 코드를 작성하는 것
목적 : 프로그램의 비정상 종료를 막고, 정상적인 실행 상태를 유지하는 것
uncaught exception
)는 JVM의 예외처리기 (UncaughtExceptionHandler)
가 받아서 예외의 원인을 화면에 출력한다.
🔔 두가지 경우에 따른 문장 실행순서
▶ try 불럭 내에서 예외가 발생한 경우,
(1) 발생한 예외와 일치하는 catch 블럭이 있는지 확인한다.
(2) 일치하는 catch 블럭을 찾게 되면, 그 catch 블럭 내의 문장들을 수행하고 전체 try-catch 문을 빠져나가서 그 다음 문장을 계속해서 수행한다. 만일 일치하는 catch 블럭을 찾지 못하면, 예외는 처리되지 못한다.
▶ try 블럭 내에서 예외가 발생하지 않은 경우,
(1) catch 블럭을 거치지 않고 전체 try-catch 문을 빠져나가서 수행을 계속한다.
첫 번째
catch
블럭부터 차례로 내려가면서catch
블럭의 괄호()
내에 선언된 참조 변수의 종류와 생성된 예외 클래스의 인스턴스에instanceof
연산자를 이용해서 검사하게 되는데, 검사결과가true
인catch
블럭을 만날 때까지 검사는 계속된다.
✔️ printStackTrace()와 getMessage()
printStackTrace() : 예외 발생 당시의 호출 스택 (Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.
getMessage() : 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.
✔️ 멀티 catch 블럭
- JDK 1.7 부터 여러
catch
블럭을|
기호를 이용해서, 하나의catch
블럭으로 합칠 수 있게 되었으며, 이를 멀티 catch 블럭 이라 한다.|
기호로 연결할 수 있는 예외 클래스의 개수에는 제한이 없다.- 멀티 catch 블럭에 사용되는
|
는 논리 연산자가 아니라 기호이다.
try {
...
} catch (ExceptionA e) {
e.printStackTrace();
} catch (ExceptionB e2) {
e2.printStackTrace();
}
➡️
try {
...
} catch (ExceptionA | ExceptionB e) {
e.printStackTrace();
}
|
기호로 연결된 예외 클래스가 조상과 자손의 관계에 있다면 컴파일 에러가 발생한다.try {
...
} catch (ParentException | ChildException e) { // 에러!
e.printStackTrace();
}
try {
...
} catch (ParentException e) {
e.printStackTrace();
}
|
기호로 연결된 예외 클래스들의 공통 분모인 조상 예외 클래스에 선언된 멤버만 사용할 수 있다.
try {
...
} catch (ExceptionA | ExceptionB e) {
e.methodA(); // 에러. ExceptionA에 선언된 methodA()는 호출불가
if (e instanceof ExceptionA) {
ExceptionA e1 = (ExceptionA) e;
e1.methodA(); // OK. ExceptionA에 선언된 메서드 호출가능
} else { // if ( e instanceof ExceptionB)
...
}
e.printStackTrace();
}
instanceof
로 어떤 예외가 발생한 것인지 확인하고 개별적으로 처리할 수는 있다.catch
블럭을 합칠 일은 거의 없을 것이다.catch
블럭을 멀티 catch
블럭으로 합치는 경우는 대부분 코드를 간단히 하는 정도의 수준일 것이므로 이러한 제약에 대해 너무 고민하지 않아도 된다.
- 키워드는
throw
를 사용해서 프로그래머가 고의로 예외를 발생시킬 수 있다.
순서
(1) 먼저, 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든 다음
- Exception e = new Exception("고의로 발생시켰음");
(2) 키워드 throw를 이용해서 예외를 발생시킨다.
- throw e;
class ExceptionEx9 {
public static void main(String[] args) {
try {
Exception e = new Exception("고의로 발생시켰음.");
throw e; // 예외를 발생시킴
// throw new Exception("고의로 발생시켰음."); // 위의 두 줄을 한줄로 줄여 쓸 수 있다.
} catch (Exception e) {
System.out.println("에러 메시지 : " + e.getMessage());
e.prinStackTrace();
}
System.out.println("프로그램이 정상 종료되었음.");
}
}
에러 메시지 : 고의로 발생시켰음.
java.lang.Exception : 고의로 발생시켰음.
at ExceptionEx9.main (ExceptionEx9.java : 4)
프로그램이 정상 종료되었음.
Exception
인스턴스를 생성할 때, 생성자에 String
을 넣어 주면, 이 String
이 Exception
인스턴스에 메시지로 저장된다.getMessage()
를 이용해서 얻을 수 있다.
💡 참고
- 컴파일러가 예외처리를 확인하지 않는
RuntimeException
클래스들 :unchecked 예외
- 예외처리를 확인하는
Exception
클래스 :checked 예외
- 메서드에 예외를 선언하려면, 메서드의 선언부에 키워드
throw
를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 된다.- 예외가 여러 개일 경우에는 쉼표
,
로 구분한다.
void method() throws Exception1, Exception2, ... ExceptionN {
// 메서드의 내용
}
💡 참고
예외를 발생시키는 키워드throw
와 예외를 메서드에 선언할 때 쓰이는throws
를 잘 구별하자.
RuntimeException
클래스들은 적지 않는다. throws
에 선언한다고 해서 문제가 되지는 않지만, 보통 반드시 처리해주어야 하는 예외들만 선언한다. Throws
를 보고, 이 메서드에서는 어떤 예외가 발생할 수 있으며 반드시 처리해주어야 하는 예외는 어떤 것들이 있는지 확인하는 것이 좋다.
class ExceptionEx12 {
public static void main(String[] args) throws Exception {
method1(); // 같은 클래스내의 static 멤버이므로 객체생성없이 직접 호출 가능.
} // main 메서드의 끝
static void method1() throws Exception {
method2();
} // method1의 끝
static void method2() throws Exception {
throw new Exception();
} // method2의 끝
}
java.lang.Exception
at ExceptionEx12.method2 (ExceptionEx12.java : 11)
at ExceptionEx12.method1 (ExceptionEx12.java : 7)
at ExceptionEx12.main(ExceptionEx12.java : 3)
(1) 예외가 발생했을 때, 모두 3개의 메서드 (main, method1, method2)가 호출 스택에 있었으며,
(2) 예외가 발생한 곳은 제일 윗줄에 있는 method2() 라는 것과
(3) main메서드가 method1()을, 그리고 method1()은 method2()를 호출했다는 것을 알 수 있다.
class ExceptionEx14 {
public static void main(String[] args) {
try {
method1();
} catch (Exception e) {
System.out.println("main 메서드에서 예외가 처리되었습니다.");
e.printStackTrace();
}
} // main 메서드의 끝
static void method1() throws Exception {
throw new Exception();
} // method1 ()의 끝
} // class의 끝
main 메서드에서 예외가 처리되었습니다.
java.lang.Exception
at ExceptionEx14.method1(ExceptionEx14.java : 12)
at ExceptionEx14.main(ExceptionEx14.java : 4)
ExceptionEx14
클래스처럼 예외가 발생한 메서드에서 예외를 처리하지 않고 호출한 메서드로 넘겨주면, 호출한 메서드에서는 method1()
을 호출한 라인에서 예외가 발생한 것으로 간주되어 이에 대한 처리를 하게 된다. method1()
에서 예외를 처리할 수도 있고, 예외가 발생한 메서드를 호출한 메서드 main메서드
에서 처리할 수도 있다.
import java.io.*;
class ExceptionEx15 {
public static void main(String[] args) {
// command line에서 입력받은 값을 이름으로 갖는 파일을 생성한다.
File f = createFile(args[0]);
System.out.println( f.getName() + " 파일이 성공적으로 생성되었습니다.");
} // main 메서드의 끝
static File createFile(String fileName) {
try {
if (fileName == null || fileName.equals(""));
throw new Exception("파일이름이 유효하지 않습니다.");
} catch (Exception e) {
// fileName이 부적절한 경우, 파일 이름을 '제목없음.txt'로 한다.
fileName = "제목없음.txt";
} finally {
File f = new File(fileName); // File 클래스의 객체를 만든다.
createNewFile(f); // 생성된 객체를 이요해서 파일을 생성한다.
return f;
}
} // createFile 메서드의 끝
static void createNewFile(File f) {
try {
f.createNewFile(); // 파일을 생성한다.
} catch(Exception e) { }
// createNewFile 메서드의 끝
}
}
import java.io.*;
class ExceptionEx16 {
public static void main(String[] args) {
try {
File f = createFile(args[0]);
System.out.println( f.getName() + " 파일이 성공적으로 생성되었습니다.");
} catch (Exception e) {
System.out.println(e.getMessage() + " 다시 입력해 주시기 바랍니다.");
}
} // main 메서드의 끝
static File createFile(String fileName) throws Exception {
if (fileName == null || fileName.equals(""));
throw new Exception("파일이름이 유효하지 않습니다.");
File f = new File(fileName); // File 클래스의 객체를 만든다.
// File 객체의 createNewFile 메서드를 이용해서 실제 파일을 생성한다.
f.createNewFile();
return f; // 생성된 객체의 참조를 반환한다.
}
} // createFile 메서드의 끝
} // 클래스의 끝
ExceptionEx15
와 ExceptionEx16
의 차이점은 예외의 처리방법
ExceptionEx15
는 예외가 발생한 createFile
메서드 자체 내에서 처리를 하며, ExceptionEx16
은 createFile
메서드를 호출한 메서드 main메서드
에서 처리한다. try-catch 문
을 사용해서 처리하고, 두 번째 예제처럼 메서드에 호출 시 넘겨받아야 할 값 fileName
을 다시 받아야 하는 경우 (메서드에서 자체적으로 해결이 안 되는 경우) 에는 예외를 메서드에 선언해서, 호출한 메서드에서 처리해야 한다.
finally
블럭은try-catch 문
과 함께, 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용된다.try-catch 문
의 끝에 선택적으로 덧붙여 사용할 수 있으며,try-catch-finally
의 순서로 구성된다.
try {
// 예외가 발생할 가능성이 있는 문장들을 넣는다.
} catch (Exception1 el) {
// 예외처리를 위한 문장을 적는다.
} finally {
// 예외의 발생여부에 관계없이 항상 수행되어야하는 문장들을 넣는다.
// finally 블럭은 try-catch 문의 맨 마지막에 위치해야한다.
}
try → catch → finally
의 순으로 실행되고, 예외가 발생하지 않은 경우에는 try → finally
의 순으로 실행된다.
class FinallyTest3 {
public static void main(String[] args) {
// method1()은 static 메서드 이므로 인스턴스 생성없이 직접 호출이 가능하다.
FinallyTest3.method1();
System.out.println("method1()의 수행을 마치고 main메서드로 돌아왔습니다.");
} // main 메서드의 끝
static void method1()
try {
System.out.println("method1() 이 호출되었습니다.");
return; // 현재 실행 중인 메서드를 종료한다.
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("method1 ()의 finally블럭이 실행되었습니다.");
}
} // method1 메서드의 끝
}
method1 () 이 호출되었습니다.
method1 () 의 finally블럭이 실행되었습니다.
method1 () 의 수행을 마치고 main메서드로 돌아왔습니다.
try
블럭에서 return문
이 실행되는 경우에도 finally
블럭의 문장들이 먼저 실행된 후에, 현재 실행 중인 메서드를 종료한다. catch
블럭의 문장 수행중에 return문
을 만나도 finally
블럭의 문장들은 수행된다.
- JDK 1.7 부터
try-with-resources 문
이라는try-catch 문
의 변형이 새로 추가되었다.- 15장 입출력 (
I/0
)과 관련된 클래스를 사용할 때 유용하다.- 주로 입출력에 사용되는 클래스 중에서는 사용한 후에 꼭 닫아 줘야 하는 것들이 있다.
- 그래야 사용했던 자원 (
resources
) 이 반환되기 때문이다.
// 괄호 () 안에 두 문장 이상 넣을 경우 ';'로 구분한다.
try (FileInputStream fis = new FileInputStream("score.dat");
DataInputStream dis = new DataInputStream(fis)) {
while(true) {
score = dis.readInt();
System.out.println(score);
sum += score;
}
} catch (EOFException e) {
System.out.println("점수의 총합은 " + sum + " 입니다.");
} catch (IOException ie) {
ie.printStackTrace();
}
try-with-resources 문
의 괄호 ()
안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()
를 호출하지 않아도 try
블럭을 벗어나는 순간 자동적으로 close()
가 호출된다.catch
블럭 또는 finally
블럭이 수행된다.try
블럭의 괄호()
안에 변수를 선언하는 것도 가능하며, 선언된 변수는 try
블럭 내에서만 사용할 수 있다.
class TryWithResourceEx {
public static void main(String[] args) {
try (CloseableResource cr = new CloseableResource()) {
cr.exceptionWork(false); // 예외가 발생하지 않는다.
} catch(WorkException e) {
e.printStackTrace();
} catch(CloseException e) {
e.printStackTrace();
}
System.out.println();
try (CloseableResource cr = new CloseableResource()) {
cr.exceptionWork(true); // 예외가 발생한다.
} catch(WorkException e) {
e.printStackTrace();
} catch(CloseException e) {
e.printStackTrace();
}
} // main의 끝
}
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); }
}
exceptionWork(false) 가 호출됨
close() 가 호출됨
CloseException : CloseException 발생 !!!
at CloseableResource.close(TryWithResourceEx.java)
at TryWithResourceEx.main(TryWithResourceEx.java)
exceptionWork(false) 가 호출됨
close() 가 호출됨
WorkException : WorkException 발생 !!!
at CloseableResource.exceptionWork(TryWithResourceEx.java)
at TryWithResourceEx.main(TryWithResourceEx.java)
Suppressed : CloseException : CloseException 발생 !!!
at CloseableResource.close(TryWithResourceEx.java)
at TryWithResourceEx.main(TryWithResourceExjava)
main메서드
에 두 개의 try-catch 문
이 있는데, 첫 번째 것은 close()
에서만 예외를 발생시키고, 두 번째 것은 exceptionWork()
와 close()
에서 모두 예외를 발생시킨다.WorkException
으로하고, CloseException
은 억제된 예외로 다룬다.WorkException
에 저장된다.
Throwable
에는 억제된 예외와 관련된 메서드가 정의되어 있다.
void addSuppressed(Throwable exception) : 억제된 예외를 추가
Throwable[] getSuppressed() : 억제된 예외 (배열) 를 반환
try-catch 문
을 사용했다면, 먼저 발생한 WorkException
은 무시되고, 마지막으로 발생한 CloseException
에 대한 내용만 출력되었을 것이다.
✔️ 사용자정의 예외 만들기
try-catch
문을 통해서 메서드 내에서 자체적으로 처리하고, 그 나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록 함으로써, 양쪽에서 나눠서 처리되도록 할 수 있다. 예외 되던지기 (Exception Re-throwing)
라고 한다.try-catch 문
을 사용해서 예외처리를 해줌과 동시에 메서드의 선언부에 발생할 예외를 throws
에 지정해줘야 한다는 것이다. class ExceptionEx17 {
public static void main(String[] args) {
try {
method1();
} catch (Exception e) {
System.out.println("main 메서드에서 예외가 처리되었습니다.");
}
} // main 메서드의 끝
static void method1() throws Exception {
try {
throw new Exception();
} catch (Exception e) {
System.out.println("method1 메서드에서 예외가 처리되었습니다.");
throw e; // 다시 예외를 발생시킨다.
}
} // method1 메서드의 끝
}
method1 메서드에서 예외가 처리되었습니다.
main 메서드에서 예외가 처리되었습니다.
method1()
와 main메서드
양쪽의 catch
블럭이 모두 수행되었음을 알 수 있다.
static int method1() {
try {
System.out.println("method1() 이 호출되었습니다.");
return 0; // 현재 실행 중인 메서드를 종료한다.
} catch (Exception e) {
e.printStackTrace();
return 1; // catch 블럭 내에도 return 문이 필요하다.
} finally {
System.out.println("method1()의 finally 블럭이 실행되었습니다.");
}
} // method1 메서드의 끝
return 문
의 경우, catch
블럭에도 return 문
이 있어야 한다.
static int method1() throws Exception { // 예외를 선언해야 함
try {
System.out.println("method1() 이 호출되었습니다.");
return 0; // 현재 실행 중인 메서드를 종료한다.
} catch (Exception e) {
e.printStackTrace();
// return 1; // catch 블럭 내에도 return 문이 필요하다.
throw new Exception(); // return 문 대신 예외를 호출한 메서드로 전달.
} finally {
System.out.println("method1()의 finally 블럭이 실행되었습니다.");
}
} // method1 메서드의 끝
catch
블럭에서 예외 던지기를 해서 호출한 메서드로 예외를 전달하면, return 문
이 없어도 된다.
💡 참고
finally
블럭 내에도return 문
을 사용할 수 있으며,try
블럭이나catch
블럭의return 문
다음에 수행된다.- 최종적으로
finally
블럭 내의return 문
의 값이 반환된다.
- 한 예외가 다른 예외를 발생시킬 수도 있다.
- 예를 들어 예외 A가 예외 B를 발생시켰다면, A를 B의
원인 예외 (cause exception)
라고 한다.
Throwable initCause (Throwable cause) : 지정한 예외를 원인 예외로 등록
Throwable getCause() : 원인 예외를 반환
initCause()
는 Exception
클래스의 조상인 Throwable
클래스에 정의되어 있기 때문에 모든 예외에서 사용가능하다.
try {
startInstall(); // SpaceException 발생
copyFiles();
} catch (InstallException e) { // InstallException 은
e.printStackTrace(); // SpaceException 과 MemoryException의 조상
}
InstallException
을 SpaceException
과 MemoryException
의 조상으로 해서 catch
블럭을 작성하면, 실제로 발생한 예외가 어떤 것인지 알 수 없다는 문제가 생긴다.SpaceException
과 MemoryException
의 상속 관계를 변경해야 한다는 것도 부담이다.
public class Throwable implements Serializable {
...
private Throwable cause = this; // 객체 자신 (this) 을 원인 예외로 등록
...
}
또 다른 이유는 checked 예외
를 unchecked 예외
로 바꿀 수 있도록 하기 위해서이다.
checked 예외
가 발생해도 예외를 처리할 수 없는 상황이 하나둘 발생하기 시작했다.checked 예외
를 unchecked 예외
로 바꾸면 예외 처리가 선택적이 되므로 억지로 예외 처리를 하지 않아도 된다.static void startInstall() throws SpaceException, MemoryException {
if (!enoughSpace()) // 충분한 설치 공간이 없으면 ...
throw new SpaceException("설치할 공간이 부족합니다.");
if (!enoughMemory()) // 충분한 메모리가 없으면 ...
throw new MemoryException("메모리가 부족합니다.");
}
➡️
static void startInstall() throws SpaceException {
if (!enoughSpace()) // 충분한 설치 공간이 없으면 ...
throw new SpaceException("설치할 공간이 부족합니다.");
if (!enoughMemory()) // 충분한 메모리가 없으면 ...
throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
}
MemoryException
은 Exception
의 자손이므로 반드시 예외를 처리해야하는데, 이 예외를 RuntimeException
으로 감싸버렸기 때문에 unchecked 예외
가 되었다.startInstall()
의 선언부에 MemoryException
을 선언하지 않아도 된다.initCause()
대신 RuntimeException 의 생성자
를 사용했다.➡️ RuntimeException(Throwable cause) // 원인 예외를 등록하는 생성자
✔️ 지금까지 배운 내용 관련 소스
class ChainedExceptionEx {
public static void main(String args[]) {
try {
install();
} catch (InstallException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
static void install() throws InstallException {
try {
startInstall(); // 프로그램 설치에 필요한 준비를 한다.
copyFiles(); // 파일들을 복사한다.
} catch (SpaceException e) {
InstallException ie = new InstallException("설치중 예외발생");
ie.initCause(e);
throw ie;
} catch (MemoryException me) {
InstallException ie = new
InstallException("설치중 예외발생");
ie.initCause(me);
throw ie;
} finally {
deleteTempFiles(); // 프로그램 설치에 사용된 임시파일들을 삭제한다.
} // try의 끝
}
static void startInstall() throws SpaceException, MemoryException {
if (!enoughSpace()) { // 충분한 설치 공간이 없으면...
throw new SpaceException("설치할 공간이 부족합니다.");
}
if (!enoughMemory()) { // 충분한 메모리가 없으면...
throw new MemoryException("메모리가 부족합니다.");
// throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
}
}
static void copyFiles() { /* 파일들을 복사하는 코드를 적는다. */ }
static void deleteTempFiles() { /* 임시파일들을 삭제하는 코드를 적는다.*/}
static boolean enoughSpace() {
// 설치하는데 필요한 공간이 있는지 확인하는 코드를 적는다.
return false;
}
static boolean enoughMemory() {
// 설치하는데 필요한 메모리공간이 있는지 확인하는 코드를 적는다.
return true;
}
}
class InstallException extends Exception {
InstallException(String msg) {
super(msg);
}
}
class SpaceException extends Exception {
SpaceException(String msg) {
super(msg);
}
}
class MemoryException extends Exception {
MemoryException(String msg) {
super(msg);
}
}
InstallException : 설치 중 예외발생
at ChainedExceptionEx.install(ChainedExceptionEx.java)
at ChainedExceptionEx.main(ChainedExceptionEx.java);
Caused by : SpaceException : 설치할 공간이 부족합니다.
at ChainedExceptionEx.startInstall(ChainedException.java)
at ChainedExceptionEx.install(ChainedExceptionEx.java)
... 1 more