❓ 예외 처리가 왜 필요할까?

먼저 간단한 프로그램을 만들어보자. 사용자의 입력을 받아서 그 입력을 외부 서버에 전송하는 프로그램이다. 지금은 편의를 위해 출력으로 통신이 연결된다고 가정하도록 하자.

키보드로 입력하면 Main에서 NetworkService로 메시지를 하나 보낸다. NetworkServiceNetworkClient를 쓰는데, NetworkClient는 외부 서버와 연결, 메시지 전송, 연결을 해제하는 기능을 수행한다.

 

코드로 구현해보자.

package exception.ex0;

public class NetworkClientV0 {

    private final String address;  // 접근할 외부 서버의 주소

    public NetworkClientV0(String address) {
        this.address = address;
    }

    // 외부 서버에 연결
    public String connect() {
        System.out.println(address + " 서버가 성공적으로 연결되었습니다.");
        return "Success";
    }

    // 연결한 외부 서버에 데이터 전송
    public String send(String data) {
        System.out.println(address + " 서버에 데이터 전송: " + data);
        return "Success";
    }

    // 외부 서버와 연결 해제
    public void disconnect() {
        System.out.println(address + " 서버 연결이 해제되었습니다.");
    }
}
package exception.ex0;

public class NetworkServiceV0 {

    public void sendMessage(String data) {

        String address = "http://example.com";
        NetworkClientV0 client = new NetworkClientV0(address);

        client.connect();  // 연결
        client.send(data);  // 데이터 전송
        client.disconnect();  // 서버와의 연결 해제
    }
}
package exception.ex0;

import java.util.Scanner;

public class MainV0 {
    public static void main(String[] args) {

        NetworkServiceV0 networkService = new NetworkServiceV0();

        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("전송할 데이터: ");
            String input = scanner.nextLine();
            if (input.equals("exit")) {  // exit 입력하면 프로그램 종료
                break;
            }

            networkService.sendMessage(input);
            System.out.println();
        }

        System.out.println("프로그램을 정상적으로 종료합니다.");
    }
}
전송할 데이터: hello
http://example.com 서버가 성공적으로 연결되었습니다.
http://example.com 서버에 데이터 전송: hello
http://example.com 서버 연결에 해제되었습니다.

전송할 데이터: exit
프로그램을 정상적으로 종료합니다.

 

지금은 간단한 프로그램이라 문제가 없지만, 실제 외부 서버와 통신할 때는 네트워크 오류 등 예기치 못한 문제들이 발생할 수 있다. 오류 상황을 직접 만들어서 시뮬레이션 해보도록 하자.

package exception.ex1;

public class NetworkClientV1 {

    // 접근할 외부 서버의 주소
    private final String address;

    // 연결/전송 실패 필드 추가
    public boolean connectError;
    public boolean sendError;

    public NetworkClientV1(String address) {
        this.address = address;
    }

    // 외부 서버에 연결
    public String connect() {
        if (connectError) {
            System.out.println(address + " 서버 연결 실패!");
            return "connectError...";
        }
        System.out.println(address + " 서버가 성공적으로 연결되었습니다.");
        return "Success";
    }

    // 연결한 외부 서버에 데이터 전송
    public String send(String data) {
        if (sendError) {
            System.out.println(address + " 서버에 데이터 전송 실패: " + data);
        }
        System.out.println(address + " 서버에 데이터 전송: " + data);
        return "Success";
    }

    // 외부 서버와 연결 해제
    public void disconnect() {
        System.out.println(address + " 서버 연결이 해제되었습니다.");
    }

    public void initError(String data) {
        if (data.contains("error1")) {
            connectError = true;
        }

        if (data.contains("error2")) {
            sendError = true;
        }
    }
}

위의 코드를 간단히 살펴보자면, 외부 서버와 연결 성공 여부를 판단하는 connect() 메서드, 외부 서버로의 데이터 전송 성공 여부를 판단하는 send() 메서드가 있다. 둘 다 성공적으로 수행이 되었다면 “Success” 메시지를 반환하도록 했다. initError() 메서드는 connectError, sendError 필드의 값을 true로 설정할 수 있다. 사용자의 입력 값에 “error1”이 있으면 connectError, “error2”가 있으면 sendError 오류가 발생하도록 한다.

 

package exception.ex1;

public class NetworkServiceV1_1 {

    public void sendMessage(String data) {

        String address = "http://example.com";
        NetworkClientV1 client = new NetworkClientV1(address);
        client.initError(data);  // 에러 판별 메서드 사용

        client.connect();
        client.send(data);
        client.disconnect();
    }
}
package exception.ex1;

import java.util.Scanner;

public class MainV1 {
    public static void main(String[] args) {

        NetworkServiceV1_1 networkService = new NetworkServiceV1_1();

        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("전송할 데이터: ");
            String input = scanner.nextLine();
            if (input.equals("exit")) {
                break;
            }

            networkService.sendMessage(input);
            System.out.println();
        }

        System.out.println("프로그램을 정상적으로 종료합니다.");
    }
}
전송할 데이터: hello
http://example.com 서버가 성공적으로 연결되었습니다.
http://example.com 서버에 데이터 전송: hello http://example.com 서버 연결이 해제되었습니다.

전송할 데이터: error1
http://example.com 서버 연결 실패!
http://example.com 서버에 데이터 전송: error1 http://example.com 서버 연결이 해제되었습니다.

전송할 데이터: error2
http://example.com 서버가 성공적으로 연결되었습니다.
http://example.com 서버에 데이터 전송 실패: error2 http://example.com 서버 연결이 해제되었습니다.

전송할 데이터: exit
프로그램을 정상적으로 종료합니다.

보다시피, 입력된 데이터에 따라 오류가 발생하도록 설계했다. 하지만, 연결이 실패하면 데이터를 전송할 수 없어야 하는데 보다시피 아무 문제없이 데이터를 전송할 수 있다. 이번에는 오류가 발생했을 때 오류 로그를 남겨서 향후 디버깅에 도움이 되도록 오류 로그를 남기도록 코드를 리팩토링 해보자.

package exception.ex1;

public class NetworkServiceV1_2 {

    public void sendMessage(String data) {

        String address = "http://example.com";
        NetworkClientV1 client = new NetworkClientV1(address);
        client.initError(data);

        // 오류 발생 즉시 종료 및 로그 출력
        String connectResult = client.connect();
        if (isError(connectResult)) {
            System.out.println("[네트워크 오류 발생] 오류 코드: " + connectResult);
            return;
        }

        String sendResult = client.send(data);
        if (isError(sendResult)) {
            System.out.println("[네트워크 오류 발생] 오류 코드: " + sendResult);
            return;
        }

        client.disconnect();
    }

    private static boolean isError(String connectResult) {
        return !connectResult.equals("Success");
    }
}

NetworkServiceNetworkClient를 사용하는 전체 흐름을 관리한다. 오류가 발생한 경우에 오류 로그를 남기고, 프로그램이 더 이상 진행할 수 없도록 return으로 중지하도록 했다.

 

package exception.ex1;

import java.util.Scanner;

public class MainV1 {
    public static void main(String[] args) {

        // NetworkServiceV1_1 networkService = new NetworkServiceV1_1();
        NetworkServiceV1_2 networkService = new NetworkServiceV1_2();

        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("전송할 데이터: ");
            String input = scanner.nextLine();
            if (input.equals("exit")) {
                break;
            }

            networkService.sendMessage(input);
            System.out.println();
        }

        System.out.println("프로그램을 정상적으로 종료합니다.");
    }
}

/*
전송할 데이터: hello
http://example.com 서버가 성공적으로 연결되었습니다.
http://example.com 서버에 데이터 전송: hello
http://example.com 서버 연결이 해제되었습니다.

전송할 데이터: error1
http://example.com 서버 연결 실패!
[네트워크 오류 발생] 오류 코드: connectError...

전송할 데이터: error2
http://example.com 서버가 성공적으로 연결되었습니다.
http://example.com 서버에 데이터 전송 실패: error2
[네트워크 오류 발생] 오류 코드: sendError...

전송할 데이터: exit
프로그램을 정상적으로 종료합니다.
*/

지금까지의 상황을 정리하면, connect() 메서드가 실패한 경우에 send() 메서드를 호출할 수 없도록 처리했다. 하지만, 사용 후에 disconnect() 메서드를 이용해서 연결을 해제하고 있지는 않다. connect(), send() 메서드가 실패한 경우에도 disconnect() 메서드는 마지막에 반드시 호출되어야 한다. 위의 로그처럼 “error2”로 데이터 전송에 실패하고, 연결이 해제되지 않는다면 네트워크 연결 자원들에 오류가 누적되어 향후 큰 장애가 발생할 수 있다.

 

그럼 disconnect() 메서드를 반드시 호출하도록 코드를 리팩토링 해보도록 하자.

package exception.ex1;

public class NetworkServiceV1_3 {

    public void sendMessage(String data) {

        String address = "http://example.com";
        NetworkClientV1 client = new NetworkClientV1(address);
        client.initError(data);

        String connectResult = client.connect();
        if (isError(connectResult)) {
            System.out.println("[네트워크 오류 발생] 오류 코드: " + connectResult);
        } else {
            String sendResult = client.send(data);
            if (isError(sendResult)) {
                System.out.println("[네트워크 오류 발생] 오류 코드: " + sendResult);
            }
        }

        // 연결/데이터 전송에 실패해도 disconnect() 호출
        client.disconnect();
    }

    private static boolean isError(String connectResult) {
        return !connectResult.equals("Success");
    }
}

return을 제거하고, if문으로 적절하게 분기를 사용했다. 이제 connect() 메서드가 성공한 경우에만 send() 메서드를 호출하고, 중간에 return으로 종료하지 않기 때문에 마지막에 disconnect() 메서드를 호출할 수 있다. 이제 연결에 실패해도, 데이터 전송에 실패해도 disconnect() 메서드가 호출된다.

 

package exception.ex1;

import java.util.Scanner;

public class MainV1 {
    public static void main(String[] args) {

        NetworkServiceV1_3 networkService = new NetworkServiceV1_3();

        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("전송할 데이터: ");
            String input = scanner.nextLine();
            if (input.equals("exit")) {
                break;
            }

            networkService.sendMessage(input);
            System.out.println();
        }

        System.out.println("프로그램을 정상적으로 종료합니다.");
    }
}

/*
전송할 데이터: hello
http://example.com 서버가 성공적으로 연결되었습니다.
http://example.com 서버에 데이터 전송: hello
http://example.com 서버 연결이 해제되었습니다.

전송할 데이터: error1
http://example.com 서버 연결 실패!
[네트워크 오류 발생] 오류 코드: connectError...
http://example.com 서버 연결이 해제되었습니다.

전송할 데이터: error2
http://example.com 서버가 성공적으로 연결되었습니다.
http://example.com 서버에 데이터 전송 실패: error2
[네트워크 오류 발생] 오류 코드: sendError...
http://example.com 서버 연결이 해제되었습니다.

전송할 데이터: exit
프로그램을 정상적으로 종료합니다.
*/

이제 connect() 메서드가 실패한 경우, send() 메서드를 호출하지 않아야 되는 것, 사용 후에 connect()send()에 호출에 문제가 있다 하더라도, 반드시 disconnect() 메서드를 통해 외부 서버와의 연결을 해제하는 것 모두 해결했다.

 

하지만, 반환 값으로 예외를 처리하는 NetworkServiceV1_2 , NetworkServiceV1_3와 같은 코드를 보면 정상 흐름과 예외 흐름이 전혀 분리되어 있지 않다. 이러면 코드 읽기가 매우 어렵다. 정상 흐름 코드를 보면 외부 서버와 연결, 데이터 전송, 연결 해제와 같이 매우 단순하고 직관적이다.

client.connect();
client.send(data);
client.disconnect();

 
그에 반해, NetworkServiceV1_2, NetworkServiceV1_3는 정상 흐름과 예외 흐름이 뒤섞여 있기 때문에 한눈에 파악하기가 어렵다. "가장 중요한 정상 흐름을 파악하는데 어려움이 있다는 말이다." 심지어 코드를 보면 예외 흐름이 더 많은 분량을 차지한다. 실무에서는 예외 처리가 훨씬 더 복잡할 텐데 이런 상태로 두면 나중에 매우 난감할 것이다.

// NetworkServiceV1_2 예외 처리 부분
String connectResult = client.connect();
if (isError(connectResult)) {
    System.out.println("[네트워크 오류 발생] 오류 코드: " + connectResult);
    return;
}

String sendResult = client.send(data);
if (isError(sendResult)) {
    System.out.println("[네트워크 오류 발생] 오류 코드: " + sendResult);
    return;
}

client.disconnect();
        
// NetworkServiceV1_3 예외 처리 부분
String connectResult = client.connect();
if (isError(connectResult)) {
    System.out.println("[네트워크 오류 발생] 오류 코드: " + connectResult);
} else {
    String sendResult = client.send(data);
    if (isError(sendResult)) {
        System.out.println("[네트워크 오류 발생] 오류 코드: " + sendResult);
    }
}

client.disconnect();

어떻게 하면 정상 흐름과 예외 흐름을 잘 분리할 수 있을까? 위와 같이 반환 값을 사용해서는 해결할 수 없다는 것은 확실한 것 같다. 하지만 다행스럽게도 자바에서는 이런 예외 처리 메커니즘이 존재한다.

 

🤔 참고 사항

자바의 경우 가비지 컬렉터가 있기 때문에 JVM 메모리에 있는 인스턴스는 자동으로 해제할 수 있지만, 외부 연결과 같은 자바 외부의 자원은 자동으로 해제되지 않는다. 따라서 외부 자원을 사용한 후에는 반드시 연결을 해제해서 외부 자원을 반납해야 한다.


⛓ 자바 예외 처리1 - 예외 계층

자바의 예외를 처리하기 위한 메커니즘은 프로그램의 안정성과 신뢰성을 높이는 데 아주 중요한 역할을 한다. 자바는 예외 처리를 위해 try, catch, finally, throw, throws와 같은 키워드를 사용한다. 그리고 예외를 다루기 위한 예외 처리용 객체들을 제공한다. 아래 자바의 예외 계층 그림을 보자.

  • Object: 자바에서 기본형을 제외한 모든 것은 객체다. “예외도 객체이다.” 모든 객체의 최상위 부모는 Object이므로 예외의 최상위 부모도 Object 이다.

  • Throwable: “최상위 예외이다.” 하위에 ExceptionError 가 있다.

  • Error: 메모리 부족이나 심각한 시스템 오류는 해결할 수가 없다. 애플리케이션 개발자는 이 예외를 잡으려고 해서는 안된다.

  • Exception: 체크 예외

    • 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다.
    • Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다.RuntimeException은 예외로 한다.
  • RuntimeException: 언체크 예외, 런타임 예외라고도 불린다.

    • “컴파일러가 체크 하지 않는 언체크 예외이다.”

    • RuntimeException과 그 자식 예외는 모두 언체크 예외이다.

    • RuntimeException의 이름을 따라서 RuntimeException과 그 하위 언체크 예외를 런타임 예외라고 많이 부른다.

       

🤼‍♂️ 체크 예외 vs 언체크 예외(런타임 예외)

체크 예외는 발생한 예외를 개발자가 명시적으로 처리해야 한다. 그렇지 않으면 컴파일 오류가 발생한다. 언체크 예외는 개발자가 발생한 예외를 명시적으로 처리하지 않아도 된다.

 

💥 주의점

"상속 관계에서 부모 타입은 자식을 담을 수 있다." 이 개념이 예외 처리에도 적용되는데, 상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡는다. 따라서 애플리케이션 로직에서는 Throwable 예외를 잡으면 안되는데, 앞서 이야기 한 잡으면 안되는 Error 예외도 함께 잡을 수 있기 때문이다. 애플리케이션 로직은 이런 이유로 Exception부터 필요한 예외로 생각하고 잡으면 된다.


📝 자바 예외 처리2 - 예외 기본 규칙

예외 처리는 “폭탄 돌리기” 다! 예외가 발생하면 누군가 잡아서 처리하거나, 처리할 수 없다면 밖으로 던져야 한다.

 

<예외를 처리할 수 있는 상황>

먼저 MainService를 호출한다. 그럼 ServiceClient를 호출한다. 이때 Client에서 예외가 발생하면 Client가 예외를 처리하지 못하고 밖으로(Service로) 던진다. 예외를 던지면 자신을 호출한 곳으로 흐름이 다시 돌아간다. 그렇게 Service로 예외가 전달되고 Service에서 예외가 처리됐다. 이후에 애플리케이션 로직이 정상 흐름으로 동작한다.

 

<예외를 처리하지 못하는 상황>

MainService를 실행하고 ServiceClient를 실행한다. 이때 Client에서 예외가 빵 터졌고, Client가 해당 예외를 처리할 수 없다면 밖으로 던져야 한다. 그럼 Client를 호출한 Service로 흐름이 다시 돌아간다. 그럼 Service도 예외를 잡아서 처리하거나 던져야 하는데, Service도 해주지 못한다면, Service를 호출한 Main으로 예외를 던져야 한다. 만약 자바의 main() 밖으로 예외를 던지면 예외 로그를 출력하면서 시스템이 종료된다.


✔ 자바 예외 처리3 - 체크 예외

Exception과 그 하위 예외는 모두 컴파일러가 체크하는 “체크 예외” 다. 단, RuntimeException은 예외로 한다. 체크 예외는 잡아서 처리하거나, 밖으로 던지도록 선언해야 한다. 그렇지 않으면 컴파일 오류가 발생한다. 일단 코드를 작성해보자.

 

package exception.basic.checked;

// Exception을 상속받은 예외는 체크 예외가 된다.
public class MyCheckedException extends Exception {
    public MyCheckedException(String message) {
        super(message);
    }
}

일단 예외 클래스를 만들려면 Exception을 상속 받으면 된다. Exception을 상속받은 예외는 체크 예외가 된다.

 

package exception.basic.checked;

public class Client {

    public void call() throws MyCheckedException {

        // 문제 상황 발생 -> 예외 터뜨리기
        throw new MyCheckedException("ex");
    }
}

위처럼 throw 예외라고 하면 새로운 예외를 발생시킬 수 있다. 아까도 말했다시피 예외도 객체이기 때문에 먼저 new로 생성하고 예외를 발생시켜야 한다. throws 예외는 발생시킨 예외를 메서드 밖으로 던질 때 사용하는 키워드다. throwthrows 사용을 명확히 구분하도록 하자.

 

package exception.basic.checked;

public class Service {

    Client client = new Client();

    // 예외를 잡아서 처리하는 코드
    public void callCatch() {
        try {
            client.call();  // MyCheckedException 오류로 바뀌어서 날라온다.
        } catch (MyCheckedException e) {  // 오류를 잡는다.
            // 예외 처리 로직
            System.out.println("예외 처리, message=" + e.getMessage());
        }

        System.out.println("정상 흐름");
    }

    // 체크 예외를 밖으로 던지는 코드
    // 체크 예외는 예외를 잡지 않고 밖으로 던지려면 throws 예외를 메서드에 필수로 선언해야 함.
    public void catchThrow() throws MyCheckedException {
        client.call();
    }
}

아까 MyCheckedExceptionException을 상속받았다. Exception을 상속받으면 체크 예외가 된다고 했다. 그리고 예외가 제공하는 기본 기능이 있는데, 그 중 오류 메시지를 보관하는 기능도 있다. MyCheckedException을 보면, 생성자를 통해 해당 기능을 그대로 사용하고 있다.

super(message)로 전달한 메시지는 Throwable에 있는 detailMessage에 보관된다. 그리고 getMessage() 메서드를 통해 오류 메시지를 조회할 수 있다.

 

일단 계속해서, 예외를 잡아서 처리하는 로직을 실행해보도록 하자.

package exception.basic.checked;

public class CheckedCatchMain {
    public static void main(String[] args) {

        Service service = new Service();
        service.callCatch();
        System.out.println("정상 종료");
    }
}

/*
예외 처리, message=ex
정상 흐름
정상 종료
*/

Service에서 예외를 처리했기 때문에, main() 메서드에서 “정상 종료”가 출력되는 것을 볼 수 있다. 실행 흐름을 살펴보자면, Service에서 try-catch 문으로 예외를 잡아서 불을 끄고 있는 상황이다. try-catch문 안으로 바뀌어 온 MyCheckedException이라는 폭탄을 잡아서 처리한 것이다. 처리하고 예외의 참조값을 이용해서 예외 메시지를 출력하고 있다. 그게 “예외 처리, message=ex”로 출력되고 있는 것이다. 예외 처리 메시지가 끝나면 정상 흐름으로 넘어가고, 메서드가 종료되는 시점에 메서드가 호출된 지점으로 되돌아가 “정상 종료”를 출력하는 것이다.

 

이번엔 예외를 처리하지 않고 밖으로 던지는 상황을 살펴보자.

package exception.basic.checked;

public class CheckedThrowMain {
    public static void main(String[] args) throws MyCheckedException {

        Service service = new Service();

        // 예외를 해결 못하는 상태
        service.catchThrow();
        System.out.println("정상 종료");
    }
}

/*
Exception in thread "main" exception.basic.checked.MyCheckedException: ex
	at exception.basic.checked.Client.call(Client.java:8)
	at exception.basic.checked.Service.catchThrow(Service.java:22)
	at exception.basic.checked.CheckedThrowMain.main(CheckedThrowMain.java:9)

정상 종료를 출력하기도 전에 main() 메서드 밖으로 나가버린다.
예외 메시지와 더불어 폭탄이 어디서부터 거슬러 왔는지까지 출력해준다.
*/

위의 코드를 보다시피, Service 안에서 예외를 처리하지 않고, 밖으로 던졌기 때문에 main() 메서드까지 예외가 전파된 것을 볼 수 있다. 그래서 main() 메서드에 있는 service.callThrow() 메서드 다음에 있는 “정상 종료”가 출력되지 않는다. 이처럼 예외가 main() 메서드 밖으로 던져지면 예외 정보와 스택 트레이스(Stack Trace)를 출력하고 프로그램이 종료된다. 스택 트레이스 정보를 활용하면 예외가 어디서 발생했는지, 어떤 경로를 거쳐서 폭탄이 넘어왔는지 알 수 있다.

 

이 상황의 흐름을 간단하게 정리해보자면,

public void catchThrow() throws MyCheckedException {
    client.call();
}

체크 예외를 처리할 수 없기 때문에 throws 키워드를 사용해서, 메서드명() throws 예외와 같이 밖으로 던질 예외를 필수로 지정해줘야 한다.

 

만약 체크 예외를 던지지 않으면 컴파일 오류가 발생한다. 다시 말해, throws를 지정하지 않으면 안 된다는 것이다. 다시 한번 말하지만, 체크 예외는 잡아서 직접 처리하거나, 밖으로 던져버리거나 둘 중 하나를 개발자가 명시적으로 처리해야 한다. try-catch로 잡아서 처리하든 throws를 지정해서 밖으로 예외를 던지든 해줘야 한다는 말이다.

 

🤔 참고 사항

throwsMyCheckedException의 상위 타입인 Exception을 적어줘도 MyCheckedException을 던질 수 있다. 상속 관계에 있기 때문에 부모 타입을 잡으면 그 자식까지 다 잡거나 던질 수 있는 것이다. 물론, 위의 상황처럼 정확하게 MyCheckedException만 밖으로 던지고 싶으면, throwsMyCheckedException을 적어줘야 한다.

 

🛠 체크 예외의 장단점

  • 장점: 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 훌륭한 안전 장치다. 이를 통해 개발자는 어떤 체크 예외가 발생한 것인지 쉽게 확인할 수 있다.
  • 단점: 하지만 실제로는, 개발자가 모든 체크 예외를 반드시 잡거나 던져야 하기 때문에 너무 번거롭다. 크게 신경쓰고 싶지 않은 예외까지 모두 챙겨야 하는 것이다.

❌ 자바 예외 처리4 - 언체크 예외

RuntimeException과 그 하위 예외는 언체크 예외로 분류된다. 언체크 예외는 말 그대로 “컴파일러가 예외를 체크하지 않는다”는 뜻이다. 체크 예외와 같지만, 차이가 있다면 throws를 선언하지 않고 생략할 수 있다는 점이다. 생략한 경우에는 자동으로 예외를 던진다.

 

다시 정리!

  • 체크 예외: 예외를 잡아서 처리하지 않으면 항상 throws 키워드를 사용해서 던지는 예외를 선언해야 한다.
  • 언체크 예외: 예외를 잡아서 처리하지 않아도 throws 키워드를 생략할 수 있다.

 

아래 코드로 바로 살펴보자.

package exception.basic.unchecked;

// RuntimeException을 상속받은 예외는 언체크 예외가 된다.
public class MyUncheckedException extends RuntimeException {
    public MyUncheckedException(String message) {
        super(message);
    }
}
package exception.basic.unchecked;

public class Client {
    public void call() {
        throw new MyUncheckedException("ex");
    }
}
package exception.basic.unchecked;

// UnChecked 예외는 잡거나 던지지 않아도 된다.
// 예외를 잡지 않으면 자동으로 밖으로 던진다.
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("정상 로직");
    }

    // 예외를 잡지 않아도 된다.
    // 체크 예외와 다르게 throws 예외 선언을 하지 않아도 된다.
    public void callThrow() {
        client.call();
    }
}
package exception.basic.unchecked;

public class UncheckedThrowMain {
    public static void main(String[] args) {

        Service service = new Service();
        service.callThrow();
        System.out.println("정상 종료");
    }
}

/*
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:24)
	at exception.basic.unchecked.UncheckedThrowMain.main(UncheckedThrowMain.java:7)

폭탄이 터졌는데, RuntimeException이네? 컴파일러가 체크하지 않는다.
체크는 하지 않지만, 폭탄은 터졌고 밖으로 빠져나온 것이다.
*/
package exception.basic.unchecked;

public class UncheckedCatchMain {
    public static void main(String[] args) {

        Service service = new Service();
        service.callCatch();
        System.out.println("정상 종료");
    }
}

/*
예외 처리, message=ex
정상 로직
정상 종료
*/

위 코드와 같이, 언체크 예외는 체크 예외와 다르게 throws 예외를 선언하지 않아도 된다. 그리고 예외를 밖으로 던지는 코드를 작성할 때는 아래와 같이 해도 무방하다.

public class Client {
	public void call() throws MyUncheckedException {
		throw new MyUncheckedException("ex");
	}
}

 
참고로 언체크 예외는 주로 생략하지만, 중요한 예외의 경우에는 명시적으로 선언해두면 해당 코드를 호출하는 개발자가 이런 예외가 발생한다는 점을 IDE를 통해 좀 더 편리하게 인지할 수 있다.

 

마지막으로 정리를 해보자.

🛠 언체크 예외의 장단점

  • 장점: 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다. 체크 예외의 경우 처리할 수 없는 예외를 밖으로 던지려면 항상 throws 예외를 선언해야 하지만, 언체크 예외는 이 부분을 생략할 수 있다.
  • 단점: 언체크 예외는 개발자가 실수로 예외를 누락할 수 있다. 반면에 체크 예외는 컴파일러를 통해 예외 누락을 잡아준다.

 

체크 예외와 언체크 예외의 차이는 예외를 처리할 수 없을 때 예외를 밖으로 던지는 부분에 있다. 이 부분을 필수로 선언해야 하는가 생략할 수 있는가의 차이인 것이다.

profile
도메인을 이해하는 백엔드 개발자(feat. OOP)

0개의 댓글