예외 계층 그림
예외도 객체이다. 모든 객체의 최상단은 Object이므로 Object부터 시작한다.
Throwable: 최상위 예외이다.
Error: 개발자가 해결할 수 없는 예외이다. 개발자는 해당 예외를 잡으려고 하면 안된다.
Exception: 체크 예외
Exception과 그 하위예외는 모두 컴파일러가 체크하는 체크 예외이다.
RuntinmeException: 런타임예외, 언체크 예외
Exception예외 중에서 예외적으로 컴파일러가 체크하지 않는 예외이다. RuntimeException과 그 하위 언체크 예외 모두 런타임 예외이다.
체크예외 vs 언체크 예외
체크 예외는 개발자가 명시적으로 처리해야한다. 그렇지 않으면 컴파일 오류가 발생한다.
언체크 예외는 개발자가 명시적으로 처리하지 않아도 된다.
상속관계에서는 부모가 자식을 담을 수 있다.
상위 예외를 Catch로 잡으면 그 하위예외도 함께 잡는다. 그러므로 Throwable을 예외로 잡으면 잡으면 안되는 Error까지 잡으므로, 애플리케이션 로직에서는 Exception부터 필요한 예외로 잡고 사용한다.
예외는 폭탄 돌리기와 같다. 예외를 처리할 수 없으면 던지고, 처리할 수있으면 잡아서 처리해야한다.
예외를 잡거나 던질때 지정한 예외 뿐만 아니라 그예외의 자식들도 함께 던지거나 잡을 수 있다.
만약에 예외를 처리하지 않고 mian() 밖으로 던지면 예외로그를 출력하면서 시스템이 종료 된다.
체크 예외는 try catch를 쓰거나, 메서드에 throws를 쓰거나 둘중에 하나는 무조건 해야한다.(예외를 처리하거나, 던지거나)
public class MyCheckedException extends Exception{
public MyCheckedException(String message) {
super(message);
}
}
Exception을 상속받은 예외는 체크 예외가 된다.
super를 사용하면 Throwable까지 메시지가 쭉 올라가는데 deailMessage에 해당 message가 저장이 된다.
public class Client {
public void call() throws MyCheckedException {
throw new MyCheckedException("오류 발생");
}
}
Client에서는 MyCheckedException을 던진다.
public class Service {
Client client = new Client();
//예외를 잡아서 처리하는 코드(정상흐름으로 반환)
public void callCatch(){
try {
client.call();
} catch (MyCheckedException e) {
//예외 처리로직
System.out.println("예외 처리, message = " + e.getMessage());
}
System.out.println("정상흐름");
}
public void callThrow() throws MyCheckedException {
client.call();
}
}
Service에서는 두가지 로직을 구현하였다.
예외는 무조건 try catch로 예외 처리를 하던가, throws로 예외를 밖으로 던져야한다.
callCatch에서는 catch이후에 정상흐름으로 로직을 진행하였고, callThrow에서는 throws를 사용하여 밖으로 예외를 던졌다.
public class CheckedCatchMain {
public static void main(String[] args) {
Service service = new Service();
service.callCatch();
System.out.println("정상종료");
}
}
callCatch를 수행하면 정상종료까지 나온다.
public class CheckedThrowMain {
public static void main(String[] args) throws MyCheckedException {
Service service = new Service();
//throws안하면 컴파일오류생김
service.callThrow();
System.out.println("정상종료");
}
}
callThrow를 호출하면 해당 callThrow에서 MyCheckedException을 던진다.
만약 main메서드에서 throws를 하지 않으면 컴파일 오류가 발생한다.
아니면 여기에서 try catch를 써야한다.
try{
service.callThrow();
}catch{
//예외 처리로직
}
main밖으로 MyCheckedException을 던지므로
Exception in thread "main" exception.basic.unchecked.MyUncheckedException: ex
at exception.basic.unchecked.Client.call(Client.java:5)
at exception.basic.unchecked.Service.callThrow(Service.java:29)
at
exception.basic.unchecked.UncheckedThrowMain.main(UncheckedThrowMain.java:7)
해당 오류가 발생한다.
참고로 체크예외를 밖으로 던지는경우에 해당 타입과 그 하위타입을 모두 던질 수 있다.
MyCheckedException의 상위타입인 Exception을 던져도 가능하다.
public class CheckedThrowMain {
public static void main(String[] args) throws Exception {
Service service = new Service();
//throws안하면 컴파일오류생김
service.callThrow();
System.out.println("정상종료");
}
}
로그는 동일하게 MyCheckedException으로 세부적인 오류의 내용이 나온다.
체크예외의 장단점
언체크 예외도 체크예외와 비슷하다.
단, throws를 선언하지 않고 생략가능하다. 생략한 경우 자동으로 예외를 던진다.
public class MyUncheckedException extends RuntimeException{
public MyUncheckedException(String message) {
super(message);
}
}
런타임예외를 상속받으면 언체크 예외가 된다.
public class Client {
public void call() throws MyUncheckedException {
throw new MyUncheckedException("오류 발생");
}
}
Client에서는 언체크 예외를 발생시키고 throws로 언체크 예외를 밖으로 던졌다.
public class Service {
Client client = new Client();
//예외를 잡아서 처리하는 코드(정상흐름으로 반환)
public void callCatch(){
try {
client.call();
} catch (MyUncheckedException e) {
//예외 처리로직
System.out.println("예외 처리, message = " + e.getMessage());
}
System.out.println("정상흐름");
}
public void callThrow() {
client.call();
}
}
예외를 처리하는부분은 앞에서 했던 체크예외와 같다. 다만, callThrow부분에서 thorws를 사용하지 않았는데, 생략한 경우 자동으로 밖으로 예외를 던진다.
public class UncheckedCatchMain {
public static void main(String[] args) {
Service service = new Service();
service.callCatch();
System.out.println("정상종료");
}
}
예외를 처리하는 로직은 체크나 언체크나 동일하다.
public class UnCheckedThrowMain {
public static void main(String[] args) {
Service service =new Service();
service.callThrow();
//오류발생하므로 아래 sout 실행x
System.out.println("정상종료");
}
}
예외를 밖으로 던지는 곳에서 main을 보면 언체크 예외는 throws를 사용하지 않았다.
throws를 생략하여도 언체크예외는 자동적으로 예외를 밖으로 던지기 때문에 MyUncheckedException이 발생하게 되고 아래의 정상종료라는 sout이 실행되지 않는다.
장점
단점
만약에 네트워크 서버에 문제가 발생하여 통신이 불가능하거나, DB접속이 끊긴 경우에는 예외를 잡아도 해결 할 수 있는 방법이 거의 없다.(들어가면 안되는 문자열을 넣는 그런예외랑은 다름.)
고로, 오류를 잡아서 처리하는것보다. 오류 페이지를 고객에게 보여주고, 오류 로그를 남기는 것이 더 효율적이다.
체크예외의 문제
체크예외가 발생했는데 catch를 하지 않으면 서비스로 해당 예외를 던져야한다.
서비스에서는 그럼 throws NetworkException, DatabaseException, XxxException을 반드시 전부 선언해 줘야한다.
아니면
try{
}catch(NetworkException){}
catch(DatabaseException){}
catch(XxxException){}
이런식으로 catch로 전부 잡아야한다.
이렇게 전부 던지거나, 처리하는 과정이 복잡하고, 만약 예외의 최상위 부모인
throws Exception, try{} catch(Exception e){} 이런식으로 Exception을 사용하면,
정작 진짜 잡아서 처리해야하는 오류를 Exception으로 뭉뜽그려서 처리하면 원하는 오류 처리 로직을 만들 수 없다.
해결방법
서비스에서 호출하는 클래스들이 런타임 예외를 던지도록 하자.(예외를 처리할 수 있는곳에서 잡으면 그만이다.) 그리고 예외를 공통으로 처리할 수 있는 곳을 만들어 한곳에서 처리하도록 하면된다.
어? 그런데 나는 체크예외 쓰고 싶은데요? -> 체크 예외를 쓰지 말라는게 아니라, 대부분을 런타임 예외로 만들어서 throws의 불편함을 줄이고, 꼭 체크 예외를 사용해야하는 경우만 사용하여 오류 처리 로직의 불편함을 줄이자는 것이다.
실제 예제
커넥션 에러와 send에러가 있다면, 각각 에러를 만드는 것보다, NetworkClientExcpetion을 만들고 해당 에러를 상속받는게 좋다.
public class NetworkClientException extends RuntimeException{
public NetworkClientException(String message) {
super(message);
}
}
public class ConnectionException extends NetworkClientException {
private final String address;
public ConnectionException(String address, String message) {
super(message);
this.address=address;
}
public String getAddress(){
return address;
}
}
public class SendException extends NetworkClientException{
private final String sendData;
public SendException(String message, String sendData) {
super(message);
this.sendData = sendData;
}
public String getSendData(){
return sendData;
}
}
NetworkClientException은 RuntimeException을 상속받아서 하위 자식인 ConnectionExcpetion과 SendExcpetion을 런타임예외,언체크예외로 만든다.
public class NetworkClientV1 {
private final String address;
public boolean connectError;
public boolean sendError;
public NetworkClientV1(String address){
this.address = address;
}
public void connect(){
if(connectError){
throw new ConnectionException(address,address+".서버 연결 실패");
}
System.out.println(address + "서버 연결 성공");
}
public void send(String data){
if (sendError){
throw new SendException(data,address+" 서버 데이터 전송실패: "+data);
}
System.out.println(address + "서버 데이터 전송: "+data);
}
public void disconnect(){
System.out.println(address + "서버 연결 해제");
}
public void initError(String data){
if(data.contains("error1")){
connectError=true;
}
if(data.contains("error2")){
sendError=true;
}
}
}
initError를 통해서 data에 따라 connectError,sendError의 값을 정한다.
public class NetworkService {
public void sendMessage(String data){
String address = "https://example.com";
NetworkClientV1 clientV1 = new NetworkClientV1(address);
clientV1.initError(data);
try {
clientV1.connect();
clientV1.send(data);
}finally {
clientV1.disconnect();
}
}
}
서비스에서 중요한것은, connect나 send메서드를 호출할때 throw되는 예외들을 throws를 사용하여 밖으로 던지지 않는것이다.
런타임 예외이므로 throws를 사용하지 않아도 catch하지 않으면 밖으로 자동으로 던진다.
public class Main {
public static void main(String[] args) {
NetworkService networkService = new NetworkService();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("전송할 문자: ");
String input = scanner.nextLine();
if (input.equals("exit")) {
break;
}
try {
networkService.sendMessage(input);
}
catch (Exception e) { // 모든 예외를 잡아서 처리
exceptionHandler(e);
}
System.out.println();
}
System.out.println("프로그램을 정상 종료합니다.");
}
//공통 예외 처리
private static void exceptionHandler(Exception e) {
//공통 처리
System.out.println("사용자 메시지: 죄송합니다. 알 수 없는 문제가 발생했습니다.");
System.out.println("==개발자용 디버깅 메시지==");
e.printStackTrace(System.out); // 스택 트레이스 출력
//e.printStackTrace(); // System.err에 스택 트레이스 출력
//필요하면 예외 별로 별도의 추가 처리 가능
if (e instanceof SendException sendEx) {
System.out.println("[전송 오류] 전송 데이터: " + sendEx.getSendData());
}
}
}
애플리케이션 로직에서 발생하는 온갖 예외들이 결국 Main메서드로 올라오게 된다.
exceptionHandler를 통해서 Excpetion을 사용해서 일단 잡은다음에,
사용자 메시지(오류페이지)로 고객에게 알려주고,
printStackTrace를 사용하여 스택트레이스를 출력한다.
이제, 여기서 꼭 잡아서 처리해야하는 로직이나, 오류에따라 instanceof를 사용하여서 처리한다.
결국 요약하자면,
애플리케이션에서 외부자원을 쓰는 경우 반드시 외부 자원을 해지해야한다.
따라서 finally 구문을 반드시 사용해야한다.
try{
//정상흐름
}catch{
//예외 흐름
}finally{
//반드시 호출해야 하는 마무리 흐름
}
하지만 자동으로 메서드를 호출하는 방법이있다.
public interface AutoCloseable {
void close() throws Exception;
}
public class NetworkClient implements AutoClosable{
//나머지는동일
@Override
public void close() {
System.out.println("NetworkClientV5.close");
disconnect();
}
}
public class NetworkServiceV5 {
public void sendMessage(String data) {
String address = "https://example.com";
try (NetworkClientV5 client = new NetworkClientV5(address)) {
client.initError(data);
client.connect();
client.send(data);
}
catch (Exception e) {
System.out.println("[예외 확인]: " + e.getMessage());
throw e;
}
}
}
장점