관리 메뉴

나구리의 개발공부기록

예외 처리 - 이론, 예외 처리가 필요한 이유(시작, 오류 상황 만들기, 반환 값으로 예외 처리), 자바 예외 처리(예외 계층, 예외 기본 규칙, 체크 예외, 언체크 예외) 본문

인프런 - 실전 자바 로드맵/실전 자바 - 중급 1편

예외 처리 - 이론, 예외 처리가 필요한 이유(시작, 오류 상황 만들기, 반환 값으로 예외 처리), 자바 예외 처리(예외 계층, 예외 기본 규칙, 체크 예외, 언체크 예외)

소소한나구리 2025. 1. 23. 21:33

출처 : 인프런 - 김영한의 실전 자바 - 중급1편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 예외 처리가 필요한 이유

1) 시작

(1) 예제 - 프로그램 구성도

  • 예외 처리가 필요한 이유를 알아보기 위해 사용자의 입력을 받고 입력 받은 문자를 외부 서버에 전송하는 예제를 작성
  • 네트워크를 학습하지 않았기 때문에 실제 통신하는 코드는 들어가지는 않지만 예외 처리가 필요한 상황을 이해하는데에는 충분함

(2) 실행 예시

(3) 클래스 설명

  • NetworkClient: 외부 서버와 연결하고 데이터를 전송하고 연결을 종료하는 기능을 제공
  • NetworkService: NetworkClient를 사용하여 데이터를 전송, NetworkClient를 사용하려면 연결, 전송, 연결 종료와 같은 복잡한 흐름을 제어해야하는데 이런 부분을 담당함
  • Main: 사용자의 입력을 받음
  • 전체 흐름: Main을 통해 사용자의 입력을 받으면 사용자의 입력을 NetworkService에 전달함, NetworkService는 NetworkClient를 사용해서 외부 서버에 연결하고 데이터를 전송함, 전송이 완료되면 연결을 종료

(4) NetworkClient 사용법 및 주의사항

  • connect()를 먼저 호출해서 서버와 연결
  • send(data)로 연결된 서버에 메시지를 전송
  • disconnect()로 연결을 해제
  • connect()가 실패한 경우 send()를 호출하면 안됨
  • 사용 후에는 반드시 disconnect()를 호출해서 연결을 해제해야하며 connect(), send() 호출에 오류가 있어도 disconnect()는 반드시 호출해야 함

(5-1) NetworkClientV0

  • 실제로 외부 네트워크에 접속하는 코드는 없지만 외부 네트워크와 접속한다고 가정하여 그 기능을 수행하는 클래스
  • String address: 접속할 외부 서버 주소
  • connect(): 외부 서버에 연결
  • send(String data): 연결한 외부 서버에 데이터를 전송
  • disconnect(): 외부 서버와 연결을 해제
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 + " 서버 연결 해제");
    }
}

 

(5-2) NetworkServiceV0

  • NetworkClient 사용 로직을 처리하는 클래스
  • 외부 서버 주소를 인자로 전달하여 NetworkClient를 생성하고, connect(), send(data), disconnect()를 순서대로 호출함
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();
    }
}

 

(5-3) MainV0

  • 전송할 문자를 Scanner를 통해서 입력을 받고 NetworkService.sendMessage()를 통해 메시지를 전달함
  • exit를 입력하면 프로그램을 정상 종료함
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")) {
                break;
            }
            networkService.sendMessage(input);
            System.out.println();
        }
        System.out.println("프로그램을 정상 종료합니다.");
    }
}
/* 실행 결과
전송할 문자: hi
http://example.com 서버 연결 성공
http://example.com 서버에 데이터 전송: hi
http://example.com 서버 연결 해제

전송할 문자: exit
프로그램을 정상 종료합니다.
*/

 

2) 오류 상황 만들기

(1) 오류 발생 가정

  • 외부 서버와 통신할 때는 정말 다양한 이유로 외부 서버와 연결에 실패하거나 데이터 전송에 문제가 발생하는 등의 다양한 문제가 발생함
  • 이러한 오류 상황들이 발생했다고 가정하고 예제에 적용

(2) NetworkClientV1

  • 오류 상황을 시뮬레이션 하기 위해 사용자의 입력 값을 활용
  • 연결 실패: 사용자가 입력하는 문자에 "error1"단어가 있으면 연결에 실패하며 오류 코드는 "connectError"임
  • 전송 실패: 사용자가 입력하는 문자에 "error2"단어가 있으면 데이터 전송에 실패하며 오류 코드는 "sendError"임
  • connect()와 send()에 검증 로직을 추가하여 문제가 없으면 success 코드를 반환하고 검증에 실패하면 오류를 반환
  • connectError: 해당 필드의 값이 true가 되면 연결에 실패하고 connectError 오류 코드를 반환
  • sendError: 위와 동일한 로직으로 동작하며 오류코드는 sendError 오류 코드를 반환
  • initError(String data): 해당 메서드를 통해 connectError와 sendError 필드의 값을 true로 설정할 수 있음
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);
            return "sendError";
        }

        // 전송 성공
        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;
        }
    }
}

 

(2) NetworkServiceV1_1

  • 기존 NetworkServiceV0에 initError(data)를 호출하는 코드를 추가하여 사용자 입력값을 기반으로 오류를 활성화함
package exception.ex1;

public class NetworkServiceV1_1 {
    public void sendMessage(String data) {
        // ... 기존 코드 동일 생략

        client.initError(data);
        
        // ... 기존 코드 동일 생략
    }
}

 

(3) MainV1

  • NetworkServiceV1_1을 생성하는 코드로 변경하여 프로그램을 실행
  • error1을 입력하면 서버 연결에 실패한다는 문구가 뜨고 error2를 입력하면 데이터 전송에 실패했다는 문구가 뜸
  • 문제: 그러나 연결이 실패하면 데이터 전송이 되지 않아야 하는데 여기서는 데이터를 전송함
  • 추가 요구사항: 오류가 발생했을 때 어떤 오류가 발생했는지 자세한 내역을 남기면 이후에 디버깅에 도움이 될 것이므로 오류 로그를 남김
package exception.ex1;


public class MainV1 {
    public static void main(String[] args) {
        NetworkServiceV1_1 networkService = new NetworkServiceV1_1();

        // ... 실행 코드 동일 생략
    }
}
/* 실행 결과
전송할 문자: 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
프로그램을 정상 종료합니다.
*/

 

3) 반환 값으로 예외 처리

(1-1) NetworkServiceV1_2

  • 오류가 발생한 경우 오류 코드를 출력으로 남기고 더 이상 진행되지 않도록 return을 사용하여 중지하도록 설정하여 연결에 실패하면 데이터를 전송하는 문제를 해결하였음
  • 참고로 if문의 코드를 별도의 isError()메서드로 추출하였는데, 조건의 부정문은 이해하기가 어렵기 때문에 알아보기 쉬운 이름으로 메서드를 만들어서 해당 메서드의 결과로 조건문이 실행되도록 변경함
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 boolean isError(String connectResult) {
        return !connectResult.equals("success");
    }
}

 

(1-2) MainV1 코드 변경

  • MainV1에서 NetworkServiceV1_1을 사용하도록 수정하고 코드를 실행해보면 error1을 입력하면 서버 연결이 실패하며 오류 코드를 출력하고 로직이 실행되지 않음
  • error2를 입력하면 연결은 성공하지만 데이터 전송이 실패하며 오류코드를 출력하고 이후 로직이 실행되지 않음
  • connect()가 실패한 경우 send()를 호출하면 안된다는 문제는 해결되었으나 사용 후에는 반드시 disconnect()를 호출해서 연결을 해제(호출에 오류가 있어도 반드시 호출)해야하는 부분이 해결이 되지 않았음
  • error2를 보면 연결은 되었는데 데이터 전송이 실패했지만 연결을 해제하지 않고 계속 두면 네트워크 연결 자원이 고갈될 수 있음

** 참고

  • 자바의 경우 GC가 있기 때문에 JVM메모리에 있는 인스턴스는 자동으로 해제할 수 있지만 외부 연결과 같은 자바 외부의 자원들은 자동으로 해제가 되지 않음
  • 외부 자원을 사용한 후에는 연결을 해제해서 외부 자원을 반드시 반납해야 하며 반납하지 않고 계속 두게되면 장애가 발생하게 됨
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();

    // ... 기존 코드 동일
}
/* 실행 결과
전송할 문자: 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
프로그램을 정상 종료합니다.
*/

 

(2-1) NetworkServiceV1_3

  • disconnect()를 반드시 호출하도록 코드를 작성
  • 프로그램에서 return문을 제거하고 if - else 문으로 분기를 사용하도록 변경
  • connect()에 성공해서 오류가 없는 경우에만 send()를 호출하며 중간에 return을 하지 않기 대문에 마지막에 있는 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);
            }
        }
        client.disconnect();
    }

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

 

(2-2) MainV1 - 코드 변경

  • NetworkServiceV1_3을 생성하도록 코드를 변경한 후 프로그램을 실행해보면 연결에 실패해도 disconnect()를 호출하고, 데이터 전송에 실패해도 disconnect()를 호출하도록 되어 요구사항이 모두 반영되었음
/* 실행 결과
전송할 문자: error1
http://example.com 서버 연결 실패
[네트워크 오류 발생] 오류 코드: connectError
http://example.com 서버 연결 해제

전송할 문자: error2
http://example.com 서버 연결 성공
http://example.com 서버에 데이터 전송 실패: error2
[네트워크 오류 발생] 오류 코드: sendError
http://example.com 서버 연결 해제

전송할 문자: exit
프로그램을 정상 종료합니다.
*/

 

(3) 정상 흐름과 예외 흐름

  • 지금 코드를 보면 반환 값으로 예외를 처리하는 NetworkServiceV1_2, NetworkServiceV1_3의 코드들을 보면 정상 흐름과 예외 흐름이 전혀 분리되어있지 않음
  • 어떤 부분이 정상 흐름이고 어떤 부분이 예외 흐름인지 이해하기가 너무 어렵고 심지어 예외 흐름을 처리하는 부분이 정상 흐름보다 더 많음
  • 아래처럼 정상흐름은 연결하고, 데이터 전송하고, 연결을 종료하면 끝나는데 정상 흐름과 예외 흐름이 섞이는 순간 코드가 복잡해지고 가장 중요한 정상 흐름이 한눈에 들어오지 않음
  • 어쩔 수 없이 작성한 예외 흐름을 처리하는 코드가 더 많은 분량을 차지하며 실무에서는 예외 처리가 훨씬 더 복잡함
// 정상 흐름
client.connect();
client.send(data);
client.disconnect();

// 정상 흐름 + 예외 흐름이 섞임

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();

 

(4) 해결 방안

  • 지금과 같이 반환 값을 사용해서 예외 상황을 처리하는 방식으로는 정상 흐름과 예외 흐름을 분리할 수는 없음
  • 이런 문제를 해결하기 위해 바로 예외 처리 매커니즘이 존재하는 것이며 예외 처리를 사용하면 정상 흐름과 예외 흐름을 명확하게 분리할 수 있음

2. 자바 예외 처리

1) 예외 계층

(1) 예외 계층

  • 자바는 프로그램 실행 중에 발생할 수 있는 예상치 못한 상황, 즉 예외(Exception)를 처리하기 위한 메커니즘을 제공함
  • 이는 프로그램의 안정성과 신뢰성을 높이는데 중요한 역할을 함
  • 자바의 예외 처리는 try, catch, finally, throw, throws의 키워드를 사용함

(2) 예외 계층 그림

  • Object: 자바에서 기본형을 제외한 모든 것은 객체이며 예외도 객체임, 모든 객체의 최상위 부모는 Object이므로 예외의 최상위 부모도 Object임
  • Throwable: 최상위 예외이며 하위에 Exception과 Error가 있음
  • Error: 메모리 부족이나 심각한 시스템 오류와 같이 애플리케이션에서 복구가 불가능한 시스템 예외이므로 애플리케이션 개발자는 이 예외를 잡아서 복구하려고 해서는 안됨
  • Exception: 체크 예외
    - 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외
    - Exception과 그 하위 예외는 모두 컴파일러가 체크하는 예외임 단, RuntimeException은 제외
  • RuntimeException: 언체크 예외, 런타임 예외
    - 컴파일러가 체크 하지 않는 언체크 예외로 RuntimeException과 그 자식 예외는 모두 언체크 예외임
    - RuntimeException의 이름을 따라서 RuntimeException과 그 하위 언체크 예외를 런타임 예외라고 많이 부름

(3) 체크 예외 vs 언체크 예외(런타임 예외)

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

** 주의

  • 상속 관계에서 부모 타입은 자식을 담을 수 있는 이 개념이 예외 처리에도 적용이 됨
  • 상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡아버리기 때문에 애플리케이션 로직에서 잡지말아야할 Error 예외를 함께 잡을 수 있는 Throwable 예외를 잡으면 안됨
  • 이런 이유로 애플리케이션 로직은 Exception부터 필요한 예외로 생각하고 잡으면 됨

2) 예외 기본 규칙

  • 예외는 폭탄 돌리기와 같은데, 예외가 발생하면 잡아서 처리하거나 처리할 수 없으면 밖으로 던져야 함

(1) 예외 처리

  1. Main은 Service를 호출
  2. Service는 Client를 호출
  3. Client에서 예외가 발생
  4. Client에서 예외를 처리하지 못하고 밖으로 던짐, 여기서 Client의 밖은 Client를 호출한 Service를 뜻함
  5. Service에 예외가 전달되고 Service에서 예외를 처리함, 이후에는 애플리케이션 로직이 정상 흐름으로 동작함
  6. 정상 흐름을 반환

(2) 예외 던짐

  • 예외를 처리하지 못하면 자신을 호출한 곳으로 예외를 던져야함
  • Client에서 예외가 발생했지만 계속 밖으로 던지게 되면 결국 최초 호출하는 Main까지 예외가 도달함

(3) 예외의 2가지 기본 규칙

  • 예외는 잡아서 처리하거나 밖으로 던져야만 함
  • 예외를 잡아서 던질 때 지정한 예외뿐만 아니라 그 예외의 자식들도 함께 처리할 수 있음
  • 예를 들어 Exception을 catch로 잡으면 그 하위 예외들도 모두 잡을 수 있고 throws로 던지면 그 하위 예외들도 모두 던질 수 있음

** 참고 - 예외를 계속 던지면?

  • 예외를 처리하지 못하고 계속 던지면 예외 로그를 출력하면서 시스템이 종료됨

3) 체크 예외

(1) MyCheckedException - 체크 예외

  • Exception과 그 하위 예외는 RuntimeException과 그 하위를 제외하고 모두 컴파일러가 체크하는 예외임
  • 체크 예외는 잡아서 처리하거나 밖으로 던지도록 선언해야하며 그렇지 않으면 컴파일 오류가 발생함
  • 예외 클래스를 만들려면 예외를 상속받으면 되며 Exception을 상속받은 예외는 체크 예외가 됨
  • 예외가 제공하는 기본 기능들 중에 오류 메시지를 보관하는 기능이 있는데 생성자를 통해서 해당 기능을 그대로 사용하면 편리함
  • super(message)로 전달한 메시지는 Throwable에 있는 detailMessage에 보관되며 getMessage()를 통해 조회할 수 있음
package exception.basic.checked;

/**
 * Exception을 상속 받은 예외는 체크 예외가 됨
 */
public class MyCheckedException extends Exception {

    public MyCheckedException(String message) {
        super(message);
    }
}

 

(2) Client

  • Client에서 예외를 발생시킴
  • throw 예외: 새로운 예외를 발생시킬 수 있는데 예외도 객체이기 때문에 객체를 먼저 new로 생성하고 예외를 발생 시켜야 함
  • throws 예외: 발생 시킨 예외를 메서드 밖으로 던질 때 사용하는 키워드임
  • throw, throws가 비슷하니 차이에 주의할 것
package exception.basic.checked;

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

 

(3) Service

  • client.call()메서드를 호출하면 컴파일 오류가 발생하며 try-catch로 오류를 잡거나 throws로 던지라고 IDE가 알려줌
  • callCatch(): client.call()을 호출할 때 발생한 예외를 try-catch로 잡아서 처리하는 코드
  • callThrow(): Service에서 처리하지 못한다고 판단되었을 때 Service를 호출한 곳으로 예외를 던지는 코드
package exception.basic.checked;

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("정상 흐름");
    }

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

 

(4) CheckedCatchMain - 예외를 잡아서 처리하는 코드

  • service.callCatch()에서 예외를 처리했기 때문에 main() 메서드까지 예외가 올라오지 않고 main()에서 출력하는 "정상 종료" 문구가 출력되는 것을 확인할 수 있음
package exception.basic.checked;

public class CheckedCatchMain {
    public static void main(String[] args) {
        Service service = new Service();
        service.callCatch();
        System.out.println("정상 종료");
    }
}
/* 실행 결과
예외 처리, message=ex
정상 흐름
정상 종료
*/

 

(5) 예외를 잡는 코드의 실행 순서 분석과 결과

  • 1. main() -> service.callCatch() -> client.call() [예외 발생, 던짐]
  • 2. main() -> service.callCatch() [예외 던짐] -> client.call()
  • 3. main() [정상 흐름] -> service.callCatch() -> client.call()
  • client.call()에서 발생한 예외를 service.callCatch()에서 잡았기 때문에 실행 결과에 try - catch 에서의 catch 부분이 작동함
  • catch로 예외를 잡아서 처리하고 나면 코드가 catch 블럭의 다음 라인으로 넘어가서 정상 흐름으로 작동함

(6) 체크 예외를 잡아서 try - catch로 처리하는 코드 - service.callCatch()

  • 예외를 잡아서 처리하려면 try - catch(...)를 사용해서 예외를 잡으면 됨
  • try 코드 블럭에서 발생하는 예외를 잡아서 catch로 넘겨서 catch블럭의 코드를 수행함
  • 여기서는 catch 에서 MyCheckedException 예외를 잡아서 처리했는데, 만약 try에서 발생한 예외가 catch의 대상에 없으면 예외를 잡을 수 없으므로 이때는 예외를 밖으로 던져야 함
  • catch는 해당 타입과 그 하위타입을 모두 잡을 수 있음
  • 예외도 객체이기 다형성이 적용되어 catch에 MyCheckedException의 상위 타입인 Exception을 적어주어도 MyCheckedException을 잡을 수 있음
  • 물론 정확하게 잡고자 하는 예외를 잡고 싶다면 발생하는 예외를 적어주면 됨

(7) CheckedThrowMain - 예외를 처리하지 않고 밖으로 던지는 코드

  • service.callThrow()를 호출하면 해당 메서드안에서 예외를 처리하지 않고 밖으로 던졌기 때문에 예외가 main() 메서드까지 올라오게되어 컴파일 오류가 발생함
  • 여기서는 예외를 잡지않고 밖으로 던지면 main() 밖으로 예외가 던져지고 service.callThrow() 메서드 다음에 있는 코드가 실행되지 않아 "정상 종료"가 출력되지 않음
  • 예외가 main() 밖으로 던져지면 예외 정보와 스택 트레이스(Stack Trace)를 출력하고 프로그램이 종료됨
  • 스택 트레이스 정보를 활용하면 예외가 어디서 발생했는지 어떤 경로를 거쳐서 넘어왔는지 확인할 수 있음
package exception.basic.checked;

public class CheckedThrowMain {
    public static void main(String[] args) throws MyCheckedException {
        Service service = new Service();
        service.callThrow();
        System.out.println("정상 종료");
    }
}
/* 실행 결과
Exception in thread "main" exception.basic.checked.MyCheckedException: ex
	at exception.basic.checked.Client.call(Client.java:5)
	at exception.basic.checked.Service.callThrow(Service.java:24)
	at exception.basic.checked.CheckedThrowMain.main(CheckedThrowMain.java:6)
*/

 

(8) 예외를 잡는 코드의 실행 순서

  • 1. main() -> service.callCatch() -> client.call() [예외 발생, 던짐]
  • 2. main() -> service.callCatch() [예외 던짐] -> client.call()
  • 3. main() [예외 던짐] -> service.callCatch() -> client.call()

(9) 체크 예외를 밖으로 던짐

  • 체크 예외를 처리할 수 없을 때는 throws 키워드를 사용하여 'method() throws 던질 예외'와 같이 밖으로 던질 예외를 필수로 지정해주어야 함
  • 체크 예외를 밖으로 던지지 않으면 컴파일 예외가 발생하는데 무시하고 실행해보면 예외를 잡거나 처리해야한다는 메시지가 출력됨
  • 즉, 체크 예외는 개발자가 직접 명시적으로 try - catch로 잡아서 처리하거나 또는 throws를 지정해서 예외를 밖으로 던진다는 선언을 필수로 해주어야 함
  • catch 처럼 throws로 예외를 밖으로 던지는 경우에도 해당 타입과 그 하위 타입을 모두 던질 수 있음

(10) 체크 예외의 장단점

  • 예외를 잡아서 처리할 수 없을 때 예외를 밖으로 던지는 throws 예외를 필수로 선언해야하며 그렇지 않으면 컴파일 오류가 발생하는 특징때문에 장점과 단점이 동시에 존재함
  • 장점: 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 매우 훌륭한 안전장치임, 이를 통해서 개발자는 어떤 체크 예외가 발생하는지 쉽고 빠르게 코드를 작성하는 단계에서 파악할 수 있음
  • 단점: 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에 크게 신경쓰고 싶지 않는 예외까지 모두 챙겨야 하므로 번거로움, 실무에서는 체크 예외가 정말 많이 발생함

4) 언체크 예외(런타임 예외)

(1) MyUnCheckedException - 언체크 예외

  • RuntimeException과 그 하위 예외는 언체크 예외로 분류되며 컴파일러가 예외를 체크하지 않는다는 뜻임
  • 언체크 예외는 체크 예외와 기본적으로 동일한데 차이는 예외를 던지는 throws를 선언하지 않고 생략할 수 있으며 생략한 경우 자동으로 예외를 던짐
  • 체크 예외 vs 언체크 예외
    - 체크 예외: 예외를 잡아서 처리하지 않으면 항상 throws 키워드를 사용해서 던지는 예외를 선언해야 함
    - 언체크 예외: 예외를 잡아서 처리하지 않아도 throws 키워드를 생략할 수 있음
package exception.basic.unchecked;

/**
 * RuntimeException을 상속받은 예외는 언체크 예외가 됨
 */
public class MyUncheckedException extends RuntimeException {
    public MyUncheckedException(String message) {
        super(message);
    }
}

 

(2) Client

  • 런타임 에러인 MyUncheckedException이 발생되었으나, throws 키워드를 생략할 수 있음
package exception.basic.unchecked;

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

 

(3) Service

  • callCatch(): client에서 발생한 MyUncheckedException을 잡아서 정상 로직으로 만듦
  • callThrow(): client에서 발생한 예외를 잡지 않아도 throws 예외 선언 없이 자연스럽게 예외가 상위로 올라감
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();
    }
}

 

(4) UncheckedCatchMain

  • Service에서 런타임예외를 잡는 callCatch()메서드를 호출했기 때문에 프로그램이 정상적으로 동작하며 출력됨
  • 체크 예외에서 예외를 잡는 메서드를 호출했을 때와 결과가 완전히 동일함
package exception.basic.unchecked;

public class UncheckedCatchMain {
    public static void main(String[] args) {
        Service service = new Service();
        service.callCatch();
        System.out.println("정상 종료");
    }
}
/* 실행 결과
예외 처리, message= ex
정상 로직
정상 종료
*/

 

(5) UncheckedThrowMain

  • Service에서 예외를 잡지않고 던지는 callThrow()메서드를 호출하면 컴파일 단계에선 아무런 오류가 발생하지 않기 때문에 문제가 없음
  • 그러나 프로그램을 실행하면 예외 정보와 스택 트레이스가 출력되고 "정상 종료" 메시지도 출력되지 않음
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:27)
	at exception.basic.unchecked.UncheckedThrowMain.main(UncheckedThrowMain.java:6)
*/

 

(6) 언체크 예외를 잡거나 밖으로 던지는 코드 설명

  • 언체크 예외도 service.callCatch()메서드에서처럼 필요한 경우 예외를 잡아서 처리할 수 있음
  • service.callThrow()메서드에서 처럼 throws 로 던질 예외를 선언하지 않아도 컴파일러가 이런 부분을 체크하지 않기 때문에 언체크 예외
  • 물론 언체크 예외도 체크 예외처럼 throws로 예외를 선언할 수 있음
  • 보통은 언체크 예외는 throws를 생략하지만 중요한 예외의 경우 throws로 선언해두면 해당 코드를 호출하는 개발자가 이런 예외가 발생한다는 점을 IDE를 통해 좀 더 편리하게 인지할 수 있음
  • 하지만 언체크 예외를 throws로 밖으로 던지도록 선언한다고 해서 체크 예외처럼 컴파일러를 통해서 체크할 수 있는 것은 아니며 IDE를 통해 개발자가 코드를 보고 예외가 발생한다고 인지할 수 있는 정도임

(7) 언체크 예외의 장단점

  • 언체크 예외는 예외를 잡아서 처리할 수 없을 때, 예외를 밖으로 던지는 throws 예외를 생략할 수 있는 특징 때문에 장점과 단점이 동시에 존재함(체크 예외와 정반대)
  • 장점: 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있음, 체크 예외는 throws 예외를 항상 선언해야 함
  • 단점: 개발자가 실수로 예외를 누락할 수 있음, 체크 예외는 컴파일러를 통해 예외 누락을 잡아 줌

(8) 정리

  • 체크 예외와 언체크 예외의 차이는 예외를 처리할 수 없을 때 예외를 밖으로 던지는 부분을 필수로 선언해야 하는가 생략할 수 있는가의 차이임
  • 옛날에는 컴파일러에서 오류를 검증할 수 있도록 체크예외를 많이 사용하였으나 현대 애플리케이션 개발에서는 실용적인 이유로 체크 예외를 거의 사용하지 않음