Welcome! Everything is fine.

[Java/Study] 김영한의 실전 자바 중급 1편 - 스터디 10회차 본문

Java

[Java/Study] 김영한의 실전 자바 중급 1편 - 스터디 10회차

개발곰발 2024. 12. 29.
728x90

 

인프런 강의 <김영한의 실전 자바 - 중급 1편>을 보고 정리한 내용입니다.

매주 모여 각자 정리한 내용을 기반으로 발표하고 질문 공유하는 스터디입니다.


 

섹션 9, 섹션 10은 모두 예외 처리에 대한 이론과 실습에 대한 내용으로 이루어져 있다. 먼저 예외에 대한 개념을 정리하려고 했는데 오류, 에러, 예외의 정확한 차이가 무엇인지 궁금해져 찾아보았다.

  • 오류(Error) : 넓은 의미로 프로그램의 실행 중에 발생하는 모든 문제 상황을 포괄적으로 지칭한다. 예외와 에러를 모두 포함하는 큰 개념이다.
  • 에러(Error 클래스) : 프로그래머가 제어하거나 처리할 수 없는 상황으로, 시스템 레벨에서 발생하는 치명적인 문제를 의미한다.
  • 예외(Exception) : 프로그램 실행 중에 예측 가능한 문제 상황으로, 프로그램 코드로 적절히 처리할 수 있는 오류를 의미한다.

📘예외처리가 필요한 이유

강의에서는 NetworkService, NetworkClient를 이용한 하나의 예제를 통해 예외 처리가 필요한 이유를 알아봤다.

 

정상적으로 연결이 되거나 데이터가 전송된 상황과 오류(연결 실패, 전송 실패 등)가 발생한 상황을 통해 어떻게 예외를 처리해야할지 알아봤다. 오류가 발생한 후 우리는 다음과 같은 문제들을 해결해야 했다.

  • 연결 실패 시 데이터를 전송하지 않는다.
  • 어떤 오류가 발생했는지 오류 로그를 남긴다.
  • 오류가 있더라도 사용 후 반드시 연결을 해제한다.

이런 조건을 충족시키기 위해 조건문을 이용해 분기하고, 각 상황에 따라 다른 오류 코드와 메시지를 출력하도록 했다. 또한 연결 성공 여부와 관계없이 무조건 연결을 해제하는 disconnect()를 호출한다.

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)) {
            System.out.println("[네트워크 오류 발생] 오류 코드: " + connectResult);
        } else {
            String sendResult = client.send(data);
            if (isError(sendResult)) {
                System.out.println("[네트워크 오류 발생] 오류 코드: " + sendResult);
            }
        }

        client.disconnect();
    }

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

 

하지만, 이렇게 만든 코드는 정상 흐름과 예외 흐름이 분리되어 있지 않다는 문제점이 있다. 그리고 일단 보기에 깔끔하지 못하다(...)

 

정상 흐름은 다음과 같이 매우 직관적이어서 이해하기가 쉽다. 그러나 예외를 처리하는 부분은 복잡하고 깔끔하지 못하다. 정상 흐름이 다음과 같이 한 눈에 들어오도록 변경할 필요가 있다.

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

 

try-catch-finally 구문과 같은 예외 처리 매커니즘을 통해 정상 흐름과 예외 흐름을 명확하게 분리할 수 있다.

try {
  //정상 흐름
} catch {
  //예외 흐름
} finally {
  //반드시 호출해야 하는 마무리 흐름
}

📘자바 예외 처리

예외 계층

예외 계층은 다음과 같다. 상위 예외를 잡으면 그 하위 예외까지 잡게 되기 때문에 애플리케이션 로직에서 Throwable 예외를 잡으면 Exception 뿐만 아니라 Error 예외도 함께 잡힌다. 따라서 Exception 부터 필요한 예외로 생각하고 잡아야 한다.

 

  • Object - 모든 객체의 최상위 부모
    • Throwable - 최상위 예외
      • Exception - 애플리케이션에서 사용할 수 있는 실질적인 최상위 예외, 체크 예외
        • SQLException 
        • IOException
        • RuntimeException - 컴파일러가 체크하지 않는 언체크 예외(런타임 예외)
          • NullPointerException
          • IllegalArgumentException
      • Error - 애플리케이션에서 복구가 불가능한 시스템 예외
        • outOfMemoryError

예외 기본 규칙

예외의 기본 규칙은 다음과 같다.

  1. 예외가 발생하면 처리하거나, 자신을 호출한 곳으로 예외를 던져야 한다.
  2. 예외를 잡거나 던질 때 지정한 예외뿐만 아니라 그 예외의 자식들도 함께 처리할 수 있다.

만약 끝까지 예외를 처리하지 못하고 자바 main() 밖으로 예외를 던지면 예외 로그를 출력하면서 시스템이 종료된다.

체크 예외

❔체크 예외 : 개발자가 명시적으로 처리해야 하는 예외. Exception과 그 하위 예외(RuntimeException 제외)를 말한다. 체크 예외는 잡아서 처리하거나 던지는 것 둘 중에 하나를 반드시 선택해야 한다.

  • 장점 : 개발자가 예외를 누락하지 않도록 도와줘서 어떤 체크 예외가 발생하는지 쉽게 파악할 수 있다.
  • 단점 : 모든 체크 예외를 개발자가 처리해야하기 때문에 번거롭다.

Exception을 상속받으면 체크 예외, RuntimeException을 상속받으면 언체크 예외가 된다. try-catch-finally 구문을 통해 예외를 처리할 수 있다.

// Exception을 상속받은 예외는 체크 예외가 된다.
public class MyCheckedException extends Exception {
    public  MyCheckedException(String message) {
        super(message);
    }
}
public class Client {
    public void call() throws MyCheckedException {
        //문제 상황 발생
        throw new MyCheckedException("ex");
    }
}

 

예외를 던질 때는 throws 키워드를 사용하는데, throw와의 차이를 알아놓자.

  • throws : 발생시킨 예외를 메서드 밖으로 던질 때 사용하는 키워드
  • throw : 새로운 예외를 발생시킬 수 있는 키워드
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 catchThrow() throws MyCheckedException {
        client.call();
    }
}

 

예외를 잡아서 처리하거나,

public class CheckedCatchMain {

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

 

예외를 처리하지 않고 밖으로 던질 수 있다. 따라서 main() 에 있는 service.callThrow() 메서드 다음에 있는 "정상 종료"가 출력되지 않는다.

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:5)
    at exception.basic.checked.Service.callThrow(Service.java:28)
    at exception.basic.checked.CheckedThrowMain.main(CheckedThrowMain.java:7)

언체크 예외

언체크 예외 : 런타임 예외라고도 하며, 개발자가 명시적으로 처리하지 않아도 되는 예외. RuntimeException과 그 하위 예외를 말한다. 예외를 잡아서 처리하지 않아도 throws 키워드를 생략할 수 있다.

  • 장점 : 크게 신경쓰지 않아도 되는 예외를 무시할 수 있다.
  • 단점 : 컴파일러에서 예외 누락을 잡아주지 않아 개발자가 예외를 누락할 수 있다.

언체크 예외는 예외를 잡거나 던지지 않아도 된다. 예외를 잡지 않으면 자동으로 밖으로 던진다.

// RuntimeException을 상속받은 예외는 언체크 예외가 된다.
public class MyUncheckedException extends RuntimeException {

    public MyUncheckedException(String message) {
        super(message);
    }
}
public class Client {
    public void call() {
        //문제 상황 발생
        throw new MyUncheckedException("ex");
    }
}
/*
 * 언체크 예외는 예외를 잡거나 던지지 않아도 된다.
 * 예외를 잡지 않으면 자동으로 박으로 던진다.
 */
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();
    }
}

 

언체크 예외는 체크 예외와 실행 결과가 완전히 동일하지만, 예외를 잡거나 throws 예외 선언을 하지 않아도 된다는 점이 다르다.

 

체크 예외 vs 언체크 예외

체크 예외는 예외를 처리하거나 던지는 것 중 하나를 반드시 선택해야하지만, 언체크 예외는 예외를 처리하지 않을 떄 던지지 않아도 된다.

 


📘try - catch - finally

public class NetworkClientV2 {

    private final String address;
    public boolean connectError;
    public boolean sendError;

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

    public void connect() throws NetworkClientExceptionV2 {
        if (connectError) {
            throw new NetworkClientExceptionV2("connectError", address + " 서버 연결 실패");
        }

        // 연결 성공
        System.out.println(address + " 서버 연결 성공");
    }

    public void send(String data) throws NetworkClientExceptionV2 {
        if (sendError) {
            throw new NetworkClientExceptionV2("sendError", 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;
        }
    }
}

📘예외 계층

예외를 계층화하면 어떤 장점이 있을까?

  • 부모 예외를 잡거나 던지면 자식 예외도 함께 잡거나 던질 수 있다.

📘실무 예외 처리 방안

 

📘try - with - resources