Java

[Java] 김영한의 실전 자바 - 중급편 섹션9 예외처리1 - 이론

고쩡이 2024. 5. 22. 15:23

자바 중급편 1

[Java] 김영한의 실전 자바 - 중급편 섹션9 예외처리1 - 이론

○ 예외 처리가 필요한 이유1 - 시작

간단히 사용자입력을 외부 서버에 전송하는 프로그램을 작성해본다.

NetworkClient는 외부서버연결,데이터 전송,연결 종료기능을 제공한다.(복잡한 흐름을 담당) NetworkService는 NetworkClient를 사용해 데이터를 전송한다.

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+ " 서버 연결 해제");
    }
}
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();
    }
}
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("프로그램을 정상 종료합니다.");
    }
}

실행 결과:)

○ 예외 처리가 필요한 이유2 - 오류 상황 만들기

이어서, 오류 상황을 시뮬레이션 할 수 있는 코드를 만들어보자.

  • 연결 실패: 사용자가 입력하는 문자에 "error1" 단어가 있으면 연결에 실패한다. 오류 코드는 "connectError"
  • 전송 실패: 사용자가 입력하는 문자에 "error2" 단어가 있으면 데이터 전송에 실패한다. 오류 코드는 "sendError"
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;
        }
    }
}

○ 예외 처리가 필요한 이유3 - 반환 값으로 예외 처리

네트워크 서비스단에서는 연결이 실패해도 여전히 데이터를 전송한다. 연결이 실패하면 데이터를 전송하지 않도록 해보자.

그리고 연결이 실패해도 반드시 disconnect()를 호출해서 연결을 해제하도록 해보자.

public class NetworkServiceV1 {
    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);
            return; // 오류가 발생한 경우 프로그램이 진행되지않도록 return으로 중지
        }
        String sendResult = client.send(data);
        if (isError(sendResult)) {
            System.out.println("[네트워크 오류 발생] 오류 코드: " + sendResult);
            return;
        }
        client.disconnect(); // 연결에 실패해도 disconnect() 호출
    }
    private static boolean isError(String resultCode){
        return !resultCode.equals("success");
    }
}
public static void main(String[] args) { // 메인 코드 네트워크 서비스 버전 변경
 NetworkServiceV1 networkService = new NetworkServiceV1();
 ...
}

 

🧐 정상 흐름과 예외흐름

아래 정상흐름은 단순하고 직관적이지만, 위의 네트워크 서비스는 정상흐름과 예외 흐름이 섞여있어 복잡하다.

client.connect(); // 정상 흐름 코드
client.send(data);
client.disconnect();

 

자바 예외처리를 통해 정상과 예외 흐름을 명확히 분리 할 수 있다.

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

🟢자바의 예외 계층

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

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

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

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

예외가 발생하면 잡아서 처리하거나, 처리할 수 없으면 밖으로 던져야한다.

 

예외처리를 못하면 자신을 호출한 곳으로 예외를 던져야한다. 예외를 main()밖으로 던지면 예외 로그를 출력하면서 시스템이 종료된다.
super(message)로 전달한 메시지는 throwable에 있는 detailMessage에 보관된다.

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

체크 예외는 잡아서 처리하거나, 또는 밖으로 던지도록 선언해야한다. 그렇지 않으면 컴파일 오류가 발생한다.

먼저 예외 코드를 작성한다.

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

예외도 객체이기 때문에 new 로 생성 후 예외를 발생시켜야한다.

public class Client {
    // throws; 발생시킨 예외를 메서드 밖으로 던질때 사용
    public void call() throws MyCheckedException{
        throw new MyCheckedException("ex"); //throw; 예외 발생
    }
}
/*
* Checked 예외는
* 예외를 잡아서 처리하거나,던지거나 둘중 하나를 필수로 선택해야 한다.
* */
public class Service {
    Client client = new Client();

    /*
    * 예외를 잡아서 처리
    * */
    public void callCatch() {
        try { //try 코드블럭에서 발생하는 예외를 잡아서 catch에 넘긴다.
            client.call();
        }catch (MyCheckedException e){
            System.out.println("예외 처리, message= " + e.getMessage());
        }
        System.out.println("정상 흐름");
    }
    /*
    * 체크 예외를 밖으로 던지는 코드
    * 체크 예외는 예외를 잡지 않고 밖으로 던지려면 throws 예외를 메서드에 필수로 선언해야 한다.
    * */
    public void callThrow() throws MyCheckedException {
        client.call();
    }
}

 

🔶예외를 잡아서 처리 (callCatch)

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

🔶예외를 처리 하지 않고 던지기

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

실행결과

🟢체크 예외 장단점

  • 체크 예외는 예외를 잡아서 처리X할땐, 예외를 밖으로 던지는 throws 예외 를 필수로 선언해야 한다. 그렇지
    않으면 컴파일 오류가 발생한다. 이것 때문에 장점과 단점이 동시에 존재한다.
    • 장점: 컴파일러를 통해 문제를 잡으므로 개발자는 어떤 체크 예외가 발생하는지 쉽게 파악할 수 있다.
    • 단점: 하지만 실제로는 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에, 너무 번거로운
      일이 된다.

🟢체크 vs 언체크 예외

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

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

언체크 예외는 컴파일러가 예외를 체크하지않는다. 또한 예외를 던지는 Throws를 선언하지않고 생략할수 있다.

package exception.basic.unchecked;

public class MyUncheckedException extends RuntimeException{
    public MyUncheckedException(String message) {
        super(message);
    }
}
public class Client {
    public void call() {
        throw new MyUncheckedException("ex");
    }
}
/**
 * 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("정상 로직");
    }
    /*
    * 예외를 잡지 않아도 된다. 자연스럽게 상위로 넘어간다.
    * 체크 예외와 다르게 trhows 예외 선언을 하지 않아도 된다.
    * */
    public void callThrow() {
        client.call();
    }
}

위는 체크 예외와 실행결과는 동일하다. 언체크 예외도 필요한 경우 예외를 잡아서 처리할 수 있다.

하지만 언체크 예외는 체크 예외와 다르게 throws 예외를 선언하지 않아도 된다.

🟢체크 예외 장단점

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