[251006] _ 예외 처리 1.

호두·2025년 10월 12일

✔️ Java / Spring

목록 보기
19/25

예외 처리가 필요한 이유

- 예외 : 단순히 오류를 표시하는 것.
- 예외 처리 : 개발자가 프로그램을 다시 정상적으로 돌리기 위해 대응 로직을 작성하는 것이다.

  • NetworkClient : 외부 서버와 연결하고, 데이터를 전송하고, 연결을 종료하는 기능이 있다.

  • NetworkService : NetworkClient 를 다뤄서 데이터를 전송하며 흐름을 제어한다.


✔️ 반환값으로 예외 처리

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 + " 서버에 데이터 전송 실패");
            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; // boolean 타입 멤버변수 값은 return을 하는게 아니라 멤버변수에 T/F 을 넣는 거구나.
            // 이 필드를 메인에서 출력하면 있는지 없는지 알 수 있네
        }
        if (data.contains("error2")) {
            sendError = true;
        }
    }
}

➡️ Client 로 서버 연결, 데이터 전송, 서버 연결 해제의 기능을 가지고 있어야 한다.

  • initError(String data) : main() 으로부터 받은 data 에 에러의 흔적이 있는지 판별한다. (boolean 형태)

➡️ 연결에러(connectError), 데이터 전송에러(sendError) 를 필드로 가지고 있다. 이 필드에 boolean 값을 넣는 방식으로 에러가 맞는지/아닌지 를 표시한다.


< 흐름 제어 역할 클래스 >

package exception.ex1;

public class NetworkServiceV1_3 {

    public void sendMessage(String data) {
        NetworkClientV1 client = new NetworkClientV1("http://example.com");
        client.initError(data);

        String connectResult = client.connect(); // 반환값을 받아서
        if (isError(connectResult)) { // 메서드 결과가 true 면 밑의 코드 실행 아니면 종료
            System.out.println("[네트워크 오류 발생] 오류 코드 : " + connectResult);
        } else {
            String sendResult = client.send(data);
            if (isError(sendResult)) {
                System.out.println("[네트워크 오류 발생] 오류 코드 : " + sendResult);
            }
        }
        client.disconnect(); // return 을 제거해 앞의 결과에 상관 없이 무조건 호출된다.
    }

    private static boolean isError(String resultCode) {
        return !resultCode.equals("success"); // 성공이 아닐 때 -> 오류일때! true 가 된다
    }
}
  • 기능이 있는 Client 클래스를 생성하고, main() 으로부터 받은 date 를 에러 판단 메서드인 initError() 에 전달한다.
  • client.connect() 를 통해서 연결을 시도한다. (반환값에 boolean값이 저장돼있음)
  • isError() 메서드로 initError() 로 채워둔 오류유무를 확인해 오류면 오류코드를 출력하고, 성공이면 send() 메서드로 데이터를 전송한다.

<Main 클래스>

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.println("전송할 문자 : ");
            String input = scanner.nextLine();
            if (input.equals("exit")) {
                break;
            }
            networkService.sendMessage(input); // 입력받은 데이터를 흐름제어 객체로 전송
            System.out.println();
        }
        System.out.println("프로그램을 정상 종료합니다.");
    }
}

반환값으로 처리하는 예외는 단점이 존재한다.

  1. 정상코드와 예외코드가 같이 있어 직관적이지 않아 유지보수가 어렵다.
  2. 불필요한 검사 코드가 많아진다. 예외가 나올 때마다 확인하고 반환하고 그 반환값을 가져와서 비교한 후에 오류 메세지를 전송해야한다.

예외 계층

자바는 프로그램 실행 중에 발생할 수 있는 예상치 못한 상황, 즉 예외(Exception)를 처리하기 위한 메커니즘을 제공한다. 그리고 예외를 다루기 위한 예외 처리용 객체들을 제공한다.

그림에서 보이듯이 예외 계층에도 최상위 부모는 Object이다. 예외 중에서는 Throwable 이 최상위 예외이다.

예외는 체크예외와 언체크예외 그리고 에러가 있다. 여기서 에러는 시스템 상에서 발생해 개발자가 해결할 수 없기 때문에 처리하려고 하면 안된다. 체크예외는 컴파일러가 확인하는 예외를 말한다. Exception을 상속받으면 다 체크예외이다. 언체크예외 혹은 런타임예외는 컴파일러가 확인하지 않는 예외를 말한다.

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

📌 예외 처리에도 상속 관계처럼 부모와 자식이 연결된다. 상위 예외를 잡으면 그 하위 예외까지 잡게된다.
(Throwable 예외는 잡으면 안됨. 건들면 안되는 Error도 잡아버리기 때문)


✅ 체크 예외는 반드시 처리하거나 던지거나 둘중 하나를 해야한다.

package exception.basic.checked; // **예외 클래스‼️**

public class MyCheckedException extends Exception { //Exception 을 상속 받은 예외는 체크예외가 된다.
    public MyCheckedException(String message) {
        super(message); // -> 이건 생성자 호출이란다*^^*
    }
}

➡️ 부모 클래스인 Exception의 생성자를 호출해 받은 메세지를 예외 객체 내부에 저장한다.
➡️ super(message) 로 전달한 메세지는 Throwable 에 있는 detailMessage 에 보관된다.

💡 이런 예외 타입 클래스는 항상 개발자가 정의해야 하는 것은 아니다.
1. 기본적인 예외 (파일 없음, 메모리 부족 등)
2. 사용자 정의 체크 예외 (개발자 입력이 필요한 특수한 문제)

package exception.basic.checked; // 예외가 발생하게 만들어둔 클래스

public class Client {
    public void call() throws MyCheckedException {
        throw new MyCheckedException("ex"); // 예외도 객체이기 때문에 new 로 생성을 하고 예외를 발생시켜야 한다.
    }
}

➡️ throw new MyCheckedException("ex") 으로 바로 위의 예외 클래스를 기반으로 예외가 생성되고 throws 로 호출한 곳에 던져진다.


✔️ 예외 처리

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

➡️ try-catch : try에 예외가 발생할 수도 있는 코드를 넣어둔다. 그리고 이 예외에 대한 대응 코드를 catch 블럭 안에 넣어둔다.

🚨 try-catch 와 throws는 같이 쓰는 건가?
두 경우가 있다. 보통 잡으면 마무리로 처리되고 다시 정상코드를 실행한다. 하지만, catch 블록 안에 무조건 예외를 다 처리하는 코드가 들어있지 않을 수도 있다. 이때는 일부를 처리하고 밖으로 예외를 또 던지는데 이때는 뒤에 throw 객체, throws, try-catch 가 다 있어야 한다.

package exception.basic.checked;

public class CheckedCatchMain {

    public static void main(String[] args) {
        Service service = new Service();
        service.callCatch(); // 예외를 처리했기 때문에 main() 메서드까지 예외가 올라오지 않는다.
        // 그래서 main() 에서 출력하는 "정상 종료" 문구나 출력된 것을 확인할 수 있다.
        System.out.println("정상 종료");
    }
}

✔️ 예외 던짐

package exception.basic.checked;

public class Service {
    Client client = new Client();
    
// 체크 예외를 밖으로 던지는 코드

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

➡️ call() 메서드에서 예외가 발생해서 던져지면 callThrow() 에서 바로 밖으로 던진다.
(여기는 붙잡아서 처리하는 코드가 없다.)

package exception.basic.checked;

public class CheckThrowMain {

    public static void main(String[] args) throws MyCheckedException {
        Service service = new Service();
        service.callThrow(); // main() 에서도 예외를 처리 못하면 콘솔에 스택 트레이스(빨간 글씨)가 출력되면서 프로그램이 종료된다.
        System.out.println("정상 종료");
    }
}

체크 예외 (Checked Exception)의 특징

  • 컴파일 시점 확인: 이 예외를 던지는(throw) 메서드는 반드시 메서드 시그니처에 throws MyCheckedException과 같이 예외를 선언해야 한다.

  • 의무적 처리: 이 예외를 호출하는 코드(메서드)는 반드시 try-catch 블록을 사용하여 예외를 처리하거나, 다시 throws로 던지도록 선언해야 한다. 이를 지키지 않으면 컴파일 오류가 발생한다.

⚠️ try-catch로 잡았는데 컴파일 오류가 나는 경우

  • 예외코드를 try-catch로 감싸지 않은 경우
  • try타입과 catch타입이 일치하지 않는 경우
  • 예외를 잡고 다시 던질 때, throws를 선언하지 않은 경우 (제일 흔함)


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

말 그대로 컴파일러가 예외를 체크하지 않는다. 기본적으로 체크 예외와 동일하나 예외를 던지는 throws를 선언하지 않고, 생략할 수 있다. 생략한 경우 자동으로 예외를 던진다.

예외를 잡아서 처리 안해도 throws 를 생략할 수 있다.


package exception.basic.unchecked; // 언체크 예외 클래스

public class MyUncheckedException extends RuntimeException {
    public MyUncheckedException(String message) { // 생성자
        super(message);
    }
}

➡️ 마찬가지로 예외가 발생하면 예외 메세지가 Throwable 에 저장된다.


package exception.basic.unchecked;

public class Client { // 언체크 예외 발생
    public void call() {
        throw new MyUncheckedException("ex");
    }
}

package exception.basic.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("정상 로직");

    }

    public void callThrow() { // ⚠️ 예외를 잡지 않아도 자동으로 상위에 던져진다.
        client.call();
    }
}

➡️ 발생한 예외를 catch로 잡고 정상로직으로 돌아간다.

예외를 잡아도 되고, 예외를 잡지 않아도 된다‼️ (체크 예외, 언체크 예외 공통)

하지만❗️
예외를 잡지 않는 경우는 반드시 던져야 한다. 이때 표시인 throw를 넣어야하는 체크 예외와 달리 언체크 예외는 던지라는 표시인 throws 를 입력해두지 않아도 자동으로 던져진다.

언체크 예외는 throws 를 생략해도 되지만 대부분 표시해서 개발자들이 쉽게 식별할 수 있게 한다.


package exception.basic.unchecked;


public class UncheckedCatchMain {

    public static void main(String[] args) {
        Service service = new Service();
        service.callCatch();
        System.out.println("정상 종료");
    }
}

📌 현대 개발에서는 사실 체크 예외를 거의 사용하지 않는다.


언체크 예외 장단점 🛜

장점 : 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다.
➡️ 잡아서 복구하기보다 프로그램이 비정상적으로 종료되게 두고 나중에 디버깅을 할 수 있다는 선택지가 주어진다. (복구 가능한 것만 처리)

단점 : 언체크 예외는 개발자가 실수로 예외를 누락할 수 있다.

0개의 댓글