Java 예외 처리 - try ~ catch 체크 예외
예외 처리의 Exception 과 하위 예외는 모두 컴파일러가 체크하는 예외이다.
단, RuntimeException은 언체크예외. 런타임 예외로 체크 예외라 다르다.
체크 예외는 처리하거나 또는 밖으로 던지도록 선언해주어야 한다.
그렇지 않다면 처리 못한 오류로 인해 컴파일 오류가 발생할 것이다.
체크 예외 코드
예외 발생하는 코드 작성하기
CheckedException.class
// Exception 상속 받아 체크 예외가 되었다.
public class CheckedException extends Exception {
public CheckedException(String message) {
super(message);
}
}
- super 부모에게 오류메시지를 보관하는 기능이 있다.
- Exception 상속받게 되면 모드 체크 예외이다.
- RuntimeException 은 체크예외에서 제외이다.
Client.class
public class Client {
public void call() throws CheckedException {
// 문제 발생시키기
CheckedException boom = new CheckedException("boom");
throw boom;
}
}
call 메소드는 강제로 예외로 보내는 코드로 작성하였다.
문제 발생 CheckedException 객체를 생성하고 throw 키워드로 예외가 생겨 던져야 하는 상황이다.
- throw 키워드로 새로운 예외를 발생시킨다. 예외 또한 객체이므로 객체를 먼저 new 로 생성하고 예외를 발생시킨다.
- throws 상속 키워드에서 메소드 밖으로 던지는 키워드이다.
- throw, throws 차이에 주의하자
Service.class
//
public class Service {
Client client = new Client();
/**
* 예외를 잡아 처리하는 메소드
*/
public void callCatch() {
client.call();
}
}
client.call() 호출하면 call() 메소드에 이미 에러 객체를 생성 후 던져서 반환하는 코드가 있다. 하지만 보편적인 방법으로 call() 메소드가 처리하지 못하는 에러가 있기에 Service 클래스는 던진 예외를 받아 컴파일 에러로 인해 실행할 수 없다.
Service.class 에외 처리하기
//
public class Service {
Client client = new Client();
/**
* 예외를 잡아 처리하는 메소드
*/
public void callCatch() {
try {
client.call();
} catch (CheckedException e) {
// 예외 처리 로직
System.out.println("예외 처리. Caught CheckedException Message = " + e.getMessage());
}
System.out.println("정상적인 흐름");
}
}
try ~ catch 문으로 예외를 처리하는 로직으로 만들었다. 처리가 된다면 정상적인 흐름으로 이어져 코드를 실행할 수 있다.
Service.class 예외 처리 못할 경우
//
public class Service {
Client client = new Client();
/**
* 예외를 잡아 처리하는 메소드
*/
public void callCatch() {
...
}
/**
* 예외를 처리하지 못할 때 사용하는 메소드
* 체크 예외를 잡지 않고 밖으로 던질려면 throws 메소드에 필수로 선언이 필요하다
*/
public void catchThrow() throws CheckedException{
// Service 클래스에서 해결 못하겠으니 자신을 호출한 클래스에게 전달한다.
client.call();
}
}
catchThrow() 메소드는 Service 객체가 이 예외를 처리 못하겠으니 자신을 호출한 클래스에게 예외를 던진 것이다.
정상 흐름의 callCatch()로 처리할지 catchThrow() 처리할 지 정해야 하는 것이다.
extends Exception
Exception 를 상속받은 자식은 모두 체크 예외가 된다.
앞서 작성한 CheckedException 클래스에도 Exception 상속한 것을 볼 수 있다.
CheckedException 클래스는 체크 예외가 된다.
RuntimeException 상속 받은 경우 언체크 예외가 되는데 이것은 자바 문법에서 정한 것이다.
예외가 제공하는 기본 기능들도 있는데, 이 중에는 오류 메시지를 보관하는 기능이 있으며 생성자를 통해 해당 기능을 그대로 사용한다.
체크 예외 - 계층 구조
+-----------+
| Object |
+-----+-----+
|
+-----+-----+
| Throwable | <- String detailMessage 멤버 필드로 메시지 보관
+-----+-----+
|
+-----+-----+
| Exception |
+-----+-----+
|
+-----+------------+
| CheckedException |
+------------------+
- super(messae) 로 전달한 메시지는 Throwable 객체의 detailMessage 에 보관하게 된다.
- getMessage() 메소드를 통해서 조회할 수 있다.
예외를 잡아서 처리하기
정상 흐름의 구조부터 살펴보자.
정상 흐름
+-------+
| Main |
+---+---+
↓ (1)
+---+-----+
| Service |
+----+----+
↓ (2)
+----+---+
| Client |
+--------+
- Main -> Service 정상적인 호출 흐름
- Service -> Client 정상적인 호출 흐름
던진 예외 처리하기
+-------+
| Main |
+---+---+
↑ (6) 정상 흐름으로 반환
+---+-----+
| Service | (5) 예외 처리
+----+----+
↑ (4) 예외 Service 에게 전달
+----+---+
| Client | (3) 예외 발생
+--------+
- Client 단에서 예외가 발생
- Client 단에서 예외 처리가 불가하여 자신을 호출한 Service 에게 던진다.
- Service 는 전달 받은 예외를 처리한다.
- 애플리케이션은 Main 에게 정상 흐름으로 반환한다.
예제 코드는 Client, Service 이어서 엔트리 포인트 main() 메소드를 작성한다.
Main.class
public class Main {
public static void main(String[] args) {
Service service = new Service();
service.callCatch();
System.out.println("프로그램 종료");
}
}
예외 처리. Caught CheckedException Message = boom
정상적인 흐름
프로그램 종료
Client 클래스에서 오류를 발생시켜 Service 클래스에게 던졌다.
Service는 try ~ catch 문의 CheckException 참조 값(e)을 잡아내어 메시지를 콘솔로 출력하였다. 이후에는 정상적인 흐름으로 동작한다.
- service.callCatch() 메소드가 예외로 처리하였으므로 main() 메소드까지 예외가 발생되지 않는다.
- main() 메소드는 정상적인 흐름으로 판단하고 "프로그램 종료" 메시지 출력한다.
자세히 살펴보기 - 실행 순서
- main() -> service.callCatch() -> client.call()
- client.call() 에서 예외 발생. 자신을 호출한 service.callCatch() 스택으로 던짐
- main() <- service.callCatch() <- client.call()
- service.callCatch() 메소드는 예외를 받게됨. try ~ catch 문법으로 처리 후 메소드 정상 종료
- main() < service.callCatch() <- client.call()
- service.callCatch() 정상 종료 확인 후 main() 메소드는 정상적인 프로그램 종료함
자세히 살펴보기 - service.callCatch()
public void callCatch() {
try {
client.call();
} catch (CheckedException e) {
// 예외 처리 로직
System.out.println("예외 처리. Caught CheckedException Message = " + e.getMessage());
}
System.out.println("정상적인 흐름");
}
예외 처리. Caught CheckedException Message = boom
정상적인 흐름
Client.call() 발생시킨 CheckedException 예외를 발생시켰고, 그 예외를 Service.callCatch() 잡아 확인했다.
실행 결과 catch 부분이 작동되어 문구를 콘솔로 출력하였다.
catch 예외 처리하고 난 후 코드는 catch 문법 다음 라인으로 넘어가 정상적인 흐름으로 진행하였다.
자세히 살펴보기 - try ~ catch
try {
client.call();
} catch (CheckedException e) {
// 예외 처리 로직
System.out.println("예외 처리. Caught CheckedException Message = " + e.getMessage());
}
- 예외를 try ~ catch 문법으로 예외를 잡아 처리할 수 있었다.
- try 코드 블록에서 발생한 예외는 catch 에게 전달하는 모습을 볼 수 있다.
- 주의사항. try 잡은 예외가 CheckedException 객체인데, 잡은 예외가 이 객체가 아닌 경우 밖으로 던져야 한다.
- CheckedException 예외를 잡았으므로 catch 문에서 처리한다.
잡지 못한 경우의 문구: "java: unreported exception CheckedException; must be caught or declared to be thrown"
try ~ catch 부모 클래스 지정하기
try {
client.call();
} catch (CheckedException e) {
// 예외 처리 로직
System.out.println("예외 처리. Caught CheckedException Message = " + e.getMessage());
}
예외를 잡을 때 CheckedException 으로 잡아 있는 모습을 볼 수 있다.
실제 CheckedException 객체를 살펴보면 부모는 Exception 으로 설정되어 있는데, catch 문도 CheckedException 부모의 Exception 지정한다.
try {
client.call();
} catch (Exception e) {
// 예외 처리 로직
System.out.println("예외 처리. Caught CheckedException Message = " + e.getMessage());
}
별 다른 문제 없이 정상적으로 출력한 것을 볼 수 있다.
- catch 상위 타입인 Exception 지정하여도 자식으로 되어있는 CheckedException 잡게 된다.
- 즉, catch 예외에 지정하면 그 예외의 하위 타입을 모두 잡아준다.
- 정확한 예외 확인을 위해 자식으로 선택한다면 CheckException 적어주면 된다.
- 예외 또한 객체이므로 다형성으로 되어있다.
예외를 처리하지 못하고 모두 던진 경우
예외를 모두 처리를 못하여 던진 경우를 살펴보자
예외 발생 - 계층 구조
+-------+
| Main |
+---+---+
↑ (6) 예외 Main 에게 전달
+---+-----+
| Service | (5) 예외 처리 불가
+----+----+
↑ (4) 예외 Service 에게 전달
+----+---+
| Client | (3) 예외 발생
+--------+
- Client 는 내가 처리 못하여 예외를 발생시켰다.
- Client -> Service 예외가 던져졌고, Service 에서도 처리하지 못하였다.
- Service -> Main 예외가 던져지고, Main에서도 처리하지 못하였다.
예제 코드로 상단 그림같이 구성하기
Client.class
public class Client {
public void call() throws CheckedException {
// 문제 발생시키기
CheckedException boom = new CheckedException("boom");
throw boom;
}
}
Service.class
//
public class Service {
Client client = new Client();
/**
* 예외를 처리하지 못할 때 사용하는 코드
* 체크 예외를 잡지 않고 밖으로 던질려면 throws 메소드에 필수로 선언이 필요하다
*/
public void catchThrow() throws CheckedException{
// Service 클래스에서 해결 못하겠으니 자신을 호출한 클래스에게 전달한다.
client.call();
}
}
Main.class
import java.util.Scanner;
public class Main {
public static void main(String[] args) throws CheckedException {
Service service = new Service();
service.catchThrow();
System.out.println("프로그램 종료 ");
}
}
Exception in thread "main" CheckedException: boom
at Client.call(Client.java:4)
at Service.catchThrow(Service.java:24)
at Main.main(Main.java:7)
실행하면 엔트리 포인트 main() 처리를 못하였으므로 빨강 오류로 문구를 표시하였다.
그리고 아래의 호출한 스택을 볼 수 있는데. 이것을 스택 트레이스(Stack Trace)라 한다.
- Service.callThrow() 예외를 처리 못한 코드로 밖으로 던지는 메소드이다. main() 메소드까지 올라오게 되었다.
- main() 예외를 받게 되었고 처리하지 못하여 main() 밖으로 던졌다. 그로 인해 정상 적인 "프로그램 종료"를 출력하지 못하고 종료되었다.
- 예외가 main() 밖으로 던져지게 된다면 예외 정보 및 스택 트레이스를 출력하여 종료한다.
- 스택 트레이스 정보를 활용하여 디버깅할 수 있으며 이 정보로 예외가 왜 발생했고, 어떤 경로로 넘어왔는지 살펴볼 수 있다.
체크 예외를 밖으로 던지지 못한 경우
체크 예외를 밖으로 던지지 못한 경우 컴파일 오류가 발생한다.
public void catchThrow() {
// Service 클래스에서 해결 못하겠으니 자신을 호출한 클래스에게 전달한다.
client.call();
}
- client.call() 호출 시 CheckedException 발생하였는데 호출한 쪽에서 체크 예외가 없다.
- 그 즉시 "java: unreported exception CheckedException; must be caught or declared to be thrown" 문구를 출력하게 된다.
- 이 문구는 예외를 잡거나 던져야한다는 경고문이다.
- 체크 예외는 직접 처리 또는 밖으로 던지라는 개발자의 명시적인 처리가 필요하다.
- 체크 예외는 try ~ catch 잡아 throws 지정하여 예외를 밖으로 던진다는 선언이 필수이다.
체크 예외를 밖으로 던지도록 해주기 (throws)
public void catchThrow() throws CheckedException{
// Service 클래스에서 해결 못하겠으니 자신을 호출한 클래스에게 전달한다.
client.call();
}
- throws 부분에 catch 에서 했던 것처럼 CheckedException의 Exception 부모 클래스도 지정하여도 된다.
- throws 지정한 타입 및 하위 타입 모두 밖으로 던질 수 있다.
- 예외도 객체이므로 다형성이 적용된다.
체크 예외 요약
체크 예외를 잡아서 처리할 수 없을 때는 예외를 밖으로 던져 throws 필수 선언이 필요하다. 그렇지 못한 경우 컴파일 오류가 발생되는데, 이것으로 인한 장단점이 있다.
- 개발자가 실수로 예외를 누락하지 않도록 문제를 명확하여 쉽게 파악할 수 있다.
- 단점으로는 개발자가 예외를 잡거나 던지도록 처리가 필요한데, 모든 객체에 대해서 작업하려면 매우 번거롭다. 신경쓰지 않아도 되는 부분까지 챙기는 문제가 생긴다.
- 신규로 테스트 용도의 hello() 객체 작성 시 client의 메소드를호출하였을 뿐인데, 컴파일 오류로 실행할 수 없다. throws Exception 키워드가 있어야 하는데, 매번 넣는 것은 번거롭다.