
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