에러는 발생 시점에 따라 다음과 같이 구분함
컴파일 에러 (Compile-time Error): 컴파일 도중(*.java
→ *.class
)에 발생하는 에러
런타임 에러 (Run-time Error): 애플리케이션의 실행 도중에 발생하는 에러
이외에도 논리적 에러가 있음
런타임 에러는 2가지로 구분함
에러 (Error)
코드를 사용하여 수습할 수 없는 심각한 오류
ex) 메모리부족(OutofMemoryError
), 스택오버플로우(StackOverflowError
)
예외 (Exception)
코드로 수습할 수 있는 상대적으로 미약한 오류
ex) 입출력작업오류(IOException
), 파일찾기/권한오류(FileNotFoundException
)
Java
에서는 에러와 예외 모두 클래스로 정의되어있음. (Object
클래스의 자손인 셈)
Exception
클래스(예외의 최고 조상) 밑으로 여러 예외 클래스들이 존재하는데,
= 외부 요인(사용자의 동작)으로 발생할 수 있는 예외들
이중에서 RuntimeException
클래스는 밑으로 또 여러 가지 하위 예외 클래스들이 있음.
= 주로 프로그래머의 실수에 의해 발생할 수 있는 예외들
예외의 발생에 대비해 코드를 작성해 놓는 것. 애플리케이션의 비정상적 종료를 막기 위함
처리되지 못한 예외(uncaught exception)는
JVM
의 예외처리기(UncaughtExceptionHandler
)가 받아서 화면에 원인을 출력해줌
try-catch
문으로 예외를 처리할 수 있음
try {
// 예외 발생 가능성이 있는 문장들
} catch (Exception1 e1) {
// Exception1이 발생했을 때, 처리를 위한 문장들
} catch (Exception2 e2) {
// Exception2가 발생했을 때, 처리를 위한 문장들
} ...
catch
블럭은 1개 이상이 올 수 있고, 예외가 발생하면 이 중 일치하는 항목이 담긴 블럭이 실행됨
예외가 발생했는데 이 중 일치하는 항목이 없으면 예외 처리에 실패함
경우에 따라 try
블럭이나 catch
블럭 안에 또 다른 try-catch
문이 들어갈 수 있음
try {
try {
...
} catch (Exception1 e1) {
...
}
...
} catch (Exception2 e2) {
try {
...
} catch (Exception3 e3) {
...
}
...
}
catch
블럭에 있는 변수는 해당 블럭 안에서만 유효하므로 e1
과 e2
가 같은 이름이었어도 상관 없음.
그러나 e2
와 e3
는 같은 블럭 안에 있으므로 같은 이름으로 사용하면 안됨.
class ExceptionExample {
public static void main(String[] args) {
int number = 100;
int result = 0;
for (int i = 0; i < 10; i++) {
result = number / (int)(Math.random() * 10); // number를 0~9 사이의 임의의 정수로 나누는 작업 (0으로 나눌 시 에러 소지 있음)
System.out.println(result);
}
}
}
위 예시는 정수를 0으로 나누는 작업이 일어날 수 있기 때문에 실행하다보면 ArithmeticException
이 발생하고 비정상 종료됨
그래서 에러 발생 가능성이 있는 지점에서 try-catch
문을 사용해서 비정상 종료를 방지할 수 있음
class ExceptionExample {
public static void main(String[] args) {
int number = 100;
int result = 0;
for (int i = 0; i < 10; i++) {
try {
result = number / (int)(Math.random() * 10);
System.out.println(result);
} catch (AritmeticException e) {
System.out.println("0"); // 정수를 0으로 나누는 케이스가 나올 경우, 0을 출력하도록 함
}
}
}
}
Case 1) try
블럭 내에서 예외가 발생한 경우
try
블럭 내 예외 발생지점 이후의 문장 무시 → 발생한 예외와 일치하는 catch
블럭 확인 → 일치하는 catch
블럭 내의 문장 실행 → 전체 try-catch
문을 빠져나가 다음 문장 실행
일치하는
catch
블럭이 없는 경우, 예외 처리에 실패하여 애플리케이션이 비정상 종료됨
Case 2) try
블럭 내에서 예외가 발생하지 않은 경우
전체 try-catch
문을 빠져나가 다음 문장 실행
📌 [유의] Case 1의 경우
try
블럭 내 예외 발생지점 이후의 문장이 무시된다는 점을 유의하자.
예외가 처리되는 과정의 내부 로직을 보자.
try
블럭 내에서 예외가 발생하면, 예외에 해당하는 클래스의 인스턴스가 생성됨
위에서 본 예시에서는
ArithmeticException
의 인스턴스 생성
생성된 예외 클래스의 인스턴스를 가지고 첫번째 catch
블럭부터 차례대로 내려가면서 괄호 내에 선언된 참조변수의 타입과 instanceof
연산자로 검사를 거침
true
가 반환되는 catch
블럭의 문장들을 수행
true
를 반환하는 블럭이 아무 것도 없다면 예외 처리 실패
전체 try-catch
문을 빠져나옴
catch
블럭 괄호의 참조변수 타입을 모든 예외의 조상 클래스인 Exception
으로 선언하면,
instanceof
연산 결과가 무조건 true
가 되어 모든 종류의 예외를 잡아낼 수 있다는 것을 알 수 있음
class ExceptionExample {
public static void main(String[] args) {
int number = 100;
int result = 0;
for (int i = 0; i < 10; i++) {
try {
result = number / (int)(Math.random() * 10);
System.out.println(result);
} catch (AritmeticException e) {
System.out.println("Caught an ArithmeticException");
} catch (Exception e) {
System.out.println("Caught an exception besides ArithmeticException");
// ArithmeticException을 제외한 다른 종류의 예외가 발생했다면 이 문장이 실행되었을 것임
}
}
}
}
위 예시에서는 이미 첫번째 catch
블럭이 실행되어 두번째는 실행될 일이 없겠지만,
👨💻 try-catch
문의 마지막 catch
블럭에 Exception
타입의 참조변수를 걸어놓으면 어떤 종류의 예외가 발생하더라도 최종적으로는 여기서 처리가 가능하다는 점을 보여줌.
예외 발생 시 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨 있음
java.lang
패키지의 Throwable
클래스에서 제공하는 printStackTrace
와 getMessage
를 통해 이 정보들을 얻을 수 있음
Exception
클래스가Throwable
클래스를 상속 받기 때문
메서드 | 반환 타입 | 설명 |
---|---|---|
printStackTrace | void | 예외 발생 당시 Call Stack에 있었던 메서드의 정보, 예외 메시지를 출력 |
getMessage | String | 발생한 예외 클래스의 인스턴스에 저장된 메시지를 문자열로 반환 |
예시)
class ExceptionExample {
public static void main(String[] args) {
System.out.println(1);
System.out.println(2);
try {
System.out.println(0 / 0);
System.out.println(3);
} catch (ArithmeticException e) {
e.printStackTrace();
System.out.println(e.getMessage());
}
System.out.println(4);
}
}
실행 결과)
1
2
java.lang.ArithmeticException: / by zero
at ExceptionExample.main(Time.java:6)
/ by zero
4
중간에 발생한 예외를 처리하고
main
메서드의 모든 문장을 수행한 뒤 정상적으로 종료됨
여러 catch
블럭을 |
기호를 이용해 하나의 블럭으로 합친 것 (JDK
1.7부터 지원)
예시)
아래 2개의 catch
블럭을
try {
...
} catch (Exception1 e) {
e.printStackTrace();
} catch (Exception2 e) {
e.printStackTrace();
}
멀티 catch
블럭으로 하나로 합치면,
try {
...
} catch (Exception1 | Exception2 e) {
e.printStackTrace();
}
중복되는 형식의 코드를 줄일 수 있다는 장점이 있음
단, Exception1
과 Exception2
가 조상-자손 관계라면 컴파일 에러 발생
가령,
Exception2
가Exception1
의 자손인 경우 그냥catch (Exception1 e) { e.printStackTrace(); }
라고 쓸 수 있기 때문에 컴파일러가 코드를 줄이라는 의미에서 던지는 에러
또한, catch
블럭 내에서는 |
로 연결된 예외 클래스들의 공통 조상이 가진 메서드만 쓸 수 있게됨.
if
문 조건식으로instanceof
연산을 돌려 타입을 좁힌 이후에 개별 예외 클래스의 메서드로 접근하는 것은 가능함
throw
키워드를 사용해 고의적으로 예외를 발생시킬 수 있음
new
연산자로 발생시키려는 예외 클래스의 인스턴스를 생성
Exception e = new Exception("예외 메시지를 적는 곳");
예외 메시지는
String
임
throw
키워드로 예외를 발생시키기
throw e;
1, 2단계를 합쳐 간소화하면,
throw new Exception("예외 메시지");
이 때 지정한 예외 메시지는 getMessage
메서드를 이용해서 얻을 수 있음
예외를 발생시켜보면, RuntimeException
과 하위 클래스들을 제외한 Exception
의 하위 클래스들은 예외 처리를 해주지 않으면 컴파일조차 되지 않음.
그러나 RuntimeException
과 하위 클래스들은 따로 예외 처리를 안해줘도 컴파일 시켜줌.
런타임 예외들은 프로그래머의 실수에서 나오는 예외이기 때문에 예외 처리를 강제하지 않음
선언부에 throws
키워드를 사용해 메서드에서 발생 가능한 예외들을 적어주면 됨 (예외들은 쉼표로 구분)
void method() throws Exception1, Exception2, Exception3, ... , ExceptionN {
...
}
이렇게 선언부에 예외들을 나열해놓으면, 메서드의 사용자가 미리 어떤 예외들을 처리해야할지 알 수 있음
앞서 보았듯이, RuntimeException
계열의 예외들은 예외 처리가 강제되지 않으므로 일반적으로는 메서드의 예외 선언에 포함시켜놓지 않음
선언부에 명시된 예외는 처리된게 아니고, 엄밀히는 자신을 호출할 메서드에게 전달하여 떠넘긴 것임
따라서, 결국 어느 한 곳에서는 반드시 try-catch
문으로 예외 처리를 해주어야만 애플리케이션의 비정상적 종료를 막을 수 있음
👨💻 예외가 발생한 메서드에서 예외 처리하기 vs 해당 메서드를 호출한 곳에서 예외 처리하기
예시)
import java.io.*;
class ExceptionExample {
public static void main(String[] args) {
try {
File f = createFile(args[0]); // Command Line 으로 넘겨받은 문자열을 제목으로 하는 파일을 생성
System.out.println(f.getName() + "파일 생성 완료");
} catch (Exception e) {
System.out.println(e.getMessage() + "다시 입력해주세요");
}
}
static File createFile(String fileName) throws Exception {
if (fileName == null || fileName.equals("")) {
throw new Exception("유효하지 않은 파일 이름입니다");
}
File f = new File(fileName); // File 인스턴스 생성
f.createNewFile(); // 실제 파일 생성
return f;
}
}
위 예시에서는 예외가 발생한 createFile
메서드가 아닌, 해당 메서드를 호출한 main
메서드에서 예외 처리를 진행함
여기서 예외를 발생시키는 요인은 잘못된 파일 이름인데, 이를 다시 받아올 수 있는(= 근본적 문제 해결이 가능한) 곳은
main
메서드 쪽이기 때문
이처럼 예외가 발생한 메서드에서 자체적인 해결이 안될 경우, 선언부에 예외를 선언해놓고 그 메서드를 호출한 곳 중 문제 해결이 가능한 곳에서 해결하는 것이 나음.
예외의 발생 여부와 관계 없이 실행시킬 코드를 포함하는 블럭
try-catch
문 뒤에 붙이며, 선택 사항임
try {
...
} catch (Exception e) {
...
} finally {
...
}
예외가 발생한 경우: try
→ catch
→ finally
순으로 진행
예외가 발생하지 않은 경우: try
→ finally
순으로 진행
try
나 catch
블럭에서 return
문이 있어도, 메서드를 종료하기 전에 finally
블럭이 실행된 후에 종료됨
입출력(I/O) 관련 클래스를 사용하면서 예외 처리를 해야할 때 유용함
입출력 작업 도중에 발생하는 예외, 자원 반환 과정에서 발생하는 예외 등
try {
fis = new FileInputSystem("name.dat");
dis = new DataInputSystem(fis);
...
} catch (IOException e) {
e.printStackTrace();
} finally {
dis.close();
}
입출력 작업이 끝나면 클래스를 꼭 닫아줘야해서 finally
블럭에 close
메서드를 넣었음.
그런데 문제는, close
실행 중에도 예외가 발생할 수 있다는 것.
try {
fis = new FileInputSystem("name.dat");
dis = new DataInputSystem(fis);
...
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (dis != null) dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
그래서 finally
블럭 내부에도 try-catch
문을 넣어 예외 처리를 해주었는데, 코드 가독성도 떨어지고
무엇보다도 원래의 try
블럭과 finally
블럭에서 동시에 예외가 발생하면 try
블럭에서 발생한 것은 무시되는 문제가 있음.
이런 문제점을 개선하기 위해 try-with-resources
문이 추가되었음
try (FileInputStream fis = new FileInputStream("name.dat");
DataInputStream dis = new DataInputStream(fis)) {
...
} catch (IOException e) {
...
} ...
try-with-resources
문이 적용된 모습.
try
블럭의 괄호안에 인스턴스를 생성하는 문장을 넣으면, 이 인스턴스는 try
블럭을 벗어나는 즉시 close
메서드를 호출하게 되어있음
단, 해당 클래스가
AutoCloseable
인터페이스를 구현한 것이어야 함
그리고 나서 catch
블럭이나 finally
블럭이 수행됨.
👨💻 만약 try
블럭에서 작업 관련 에러와 close
관련 에러가 동시에 발생하면 어떻게 될까?
예시)
class TryWithResourcesTest {
public static void main(String[] args) {
// Case 1: 작업 도중 예외는 발생X, close 실행 중에만 예외 발생
try (CloseableResource cr = new CloseableResource()) {
cr.work(false);
} catch (WorkException | CloseException e) {
e.printStackTrace();
}
System.out.println();
// Case 2: 작업 도중, 그리고 close 실행에서 모두 예외 발생
try (CloseableResource cr = new CloseableResource()) {
cr.work(true);
} catch (WorkException | CloseException e) {
e.printStackTrace();
}
}
}
class CloseableResource implements AutoCloseable {
public void work(boolean exception) throws WorkException {
System.out.println("work 메서드가 " + (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 message) {
super(message);
}
}
class CloseException extends Exception {
CloseException(String message) {
super(message);
}
}
예외 2개가 동시에 발생할 수는 없지만, 억제된 예외(suppressed exception)이라는 개념을 통해서는 2개 모두 출력할 수 있음.
우선 예시에서 취급하는 예외는 2가지:
WorkException
: try
블럭의 작업(work
메서드)을 수행할 때 발생할 수 있는 예외
CloseException
: try
블럭에서 작업완료 후, 자동으로 close
가 호출될 때 발생할 수 있는 예외
try
문의 괄호 안에 들어간 인스턴스의 타입인CloseableResource
는AutoCloseable
인터페이스를 구현한 클래스이므로, 자동으로close
호출이 가능
작업을 상징하는 work
메서드는 인자가 true
면 작업 에러(WorkException
)을 발생시키고, false
면 발생시키지 않음.
이 2가지 케이스를 각각의 try-with-resources
문에 담아 출력 결과를 살펴봄.
실행 결과)
work 메서드가 예외를 발생시키지 않음
close 메서드가 호출됨
CloseException: CloseException 예외가 발생함!
at CloseableResource.close(TryWithResourcesTest.java:28)
at TryWithResourcesTest.main(TryWithResourcesTest.java:5)
work 메서드가 예외를 발생시킴
close 메서드가 호출됨
WorkException: WorkException 예외가 발생함!
at CloseableResource.work(TryWithResourcesTest.java:22)
at TryWithResourcesTest.main(TryWithResourcesTest.java:11)
Suppressed: CloseException: CloseException 예외가 발생함!
at CloseableResource.close(TryWithResourcesTest.java:28)
at TryWithResourcesTest.main(TryWithResourcesTest.java:10)
Case 1에서는 예외가 1가지만 발생해서 일반적으로 알고 있는 형태로 에러가 출력되었음.
Case 2에서는 2가지 예외가 발생했는데, WorkException
을 대표로 하고 그 안에 Suppressed
(억제된 예외)로 CloseException
이 포함되어 출력되었음
만약 기존
try-catch
문을 사용했다면 먼저 발생한WorkException
이 무시되고,CloseException
만 출력되었을 것임
기존에 정의되어있는 예외 말고도, 사용자가 직접 예외 클래스를 정의하여 사용할 수 있음
가능하면 완전히 새로 만들기보다는, 기존 예외 클래스를 활용하는 것이 좋음
보통 Exception
클래스나 RuntimeException
클래스로부터 상속 받아 만듦
class CustomException extends Exception {
CustomException(String message) {
super(message);
}
}
기존에는 주로 Exception
을 상속 받아 Checked Exception(반드시 예외처리를 해야함)로 작성하는 것이 일반적이었는데,
최근에는 RuntimeException
을 상속받아 Unchecked Exception(선택적으로 예외처리를 함)로 작성하는 추세임.
Checked Exception은 예외 처리가 불필요한 경우까지도
try-catch
문을 넣어야해서 코드가 복잡해지기 때문
한 메서드에 발생할 수 있는 예외가 여러개일 때, 몇개는 try-catch
문으로 직접 처리하고, 나머지는 메서드 선언부에 선언하여 자신을 호출하는 다른 메서드에게 처리를 전가할 수 있음.
또는 1가지 예외에 대해서도 예외가 발생한 메서드와 그 메서드를 호출한 메서드 양쪽 모두에서 처리하도록 할 수도 있음.
이러한 방식은 예외를 처리한 후에 인위적으로 다시 발생시키는 방법인 예외 되던지기(exception re-throwing)를 통해 구현할 수 있음
class ExceptionReThrowExample {
public static void main(String[] args) {
try {
reThrow();
} catch (Exception e) {
System.out.println("main 메서드에서 예외를 처리함");
}
}
static void reThrow() throws Exception {
try {
throw new Exception();
} catch (Exception e) {
System.out.println("reThrow 메서드에서 예외를 처리함");
throw e; // 다시 예외를 발생시킴
}
}
}
reThrow
메서드에서 예외를 처리하고 나서, 다시 발생시킨 예외를 main
에서 받아 또 처리했음
원칙적으로는 try
블럭에 return
문이 존재할 경우, catch
블럭에서도 return
문이 필요한데, 예외 되던지기를 사용할 때는 catch
블럭에서 throw
로 예외를 다시 던지는 것으로 return
문을 대신할 수 있음
finally
블럭에도return
문을 넣을 수 있는데,try
와catch
블럭의return
문 다음으로 수행되며, 최종적으로는finally
의return
값이 반환됨.
예외에도 인과 관계가 성립할 수 있음. 예외 A가 예외 B를 유발했다면, A가 B의 원인 예외(cause exception라고 부름.
발생한 예외에 initCause
메서드를 사용해 원인 예외를 지정할 수 있음.
try {
...
} catch (CauseException ce) {
ResultException re = new ResultException(); // (결과)예외 생성
re.initCause(ce); // 잡아낸 예외를 원인 예외로 지정함
throw re; // (결과)예외 되던지기
}
메서드 | 매개 변수 | 반환 타입 | 설명 |
---|---|---|---|
initCause | Throwable cause | Throwable | 인자로 넘어온 예외를 원인 예외로 지정함 |
getCause | - | Throwable | 원인 예외를 반환함 |
서로 상속 관계가 아니더라도 여러 가지 예외들을 큰 분류로 묶어서 다루기 위해
원인 예외 1개로 인해 N개의 예외가 발생했다면, 원인 예외를 지정해줌으로써 N개의 예외들을 묶어서 다룰 수 있음
Checked 예외를 Unchecked 예외로 바꿀 수 있게 하기 위해
Checked 예외가 발생해도 처리할 수 없는 경우가 있는데, 이 때 실속없이
try-catch
문을 쓰기 보다는 Unchecked 예외로 바꿔주는게 나음.
initCause
를 사용하는 대신RuntimeException
의 생성자를 활용해 원인 예외의 인스턴스를 감싸주는 방법도 있음.RuntimeException(Throwable causeException); // 원인 예외를 지정하는 생성자
이렇게 해서 예외를 Unchecked로 바꾼 뒤에는 메서드 선언부의
throws
로 선언하지 않아도 됨
예시)
class ChainedExceptionExample {
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 | MemoryException e) {
InstallException ie = new InstallException("설치 작업 도중 예외 발생");
ie.initCause(e);
throw ie;
} finally {
deleteTempFiles();
}
}
static void startInstall() throws SpaceException, MemoryException {
if (!enoughSpace()) {
throw new SpaceException("설치할 공간이 부족합니다");
}
if (!enoughMemory()) {
throw new MemoryException("메모리가 부족합니다");
}
}
static void copyFiles() { ... }
static void deleteTempFiles() { ... }
static boolean enoughSpace() {
...
return false;
}
static boolean enoughMemory() {
...
return true;
}
}
class InstallException extends Exception {
InstallException(String message) {
super(message);
}
}
class SpaceException extends Exception {
SpaceException(String message) {
super(message);
}
}
class MemoryException extends Exception {
MemoryException(String message) {
super(message);
}
}
main
메서드에서의 주 작업인 install
을 진행할 때 InstallException
이 발생할 수 있음.
InstallException
의 원인 예외로는 2가지(SpaceException
, MemoryException
)가 있어 총 3종류의 사용자 정의 예외 클래스가 선언되어있음.
install
메서드의 catch
블럭에서는 두 원인 예외를 각각 잡았을 때, initCause
메서드로 원인 예외임을 지정해주고(=chaining), 그 둘을 포괄하는 InstallException
의 인스턴스를 되던짐.
startInstall
메서드에서 MemoryException
을 Unchecked로 만들어주는 방법도 있음
static void install() throws InstallException {
try {
startInstall();
copyFiles();
} catch (SpaceException e) { // MemoryExeption을 지워줬음
InstallException ie = new InstallException("설치 작업 도중 예외 발생");
ie.initCause(e);
throw ie;
} finally {
deleteTempFiles();
}
}
static void startInstall() throws SpaceException { // MemoryException을 지워줬음
if (!enoughSpace()) {
throw new SpaceException("설치할 공간이 부족합니다");
}
if (!enoughMemory()) {
throw new RuntimeException(new MemoryException("메모리가 부족합니다")); // RuntimeException의 생성자로 감싸서 Unchecked 예외로 바꿨음
}
}
실행 결과)
InstallException: 설치 작업 도중 예외 발생
at ChainedExceptionExample.install(TryWithResourcesExample.java:17)
at ChainedExceptionExample.main(TryWithResourcesExample.java:4)
Caused by: SpaceException: 설치할 공간이 부족합니다
at ChainedExceptionExample.startInstall(TryWithResourcesExample.java:27)
at ChainedExceptionExample.install(TryWithResourcesExample.java:14)
... 1 more