JAVA의 가장 기본이 되는 내용

예외처리 1 (예외처리가 필요한 이유)

cwchoiit 2024. 4. 23. 11:09
728x90
반응형
SMALL

참고자료:

 

김영한의 실전 자바 - 중급 1편 | 김영한 - 인프런

김영한 | 실무에 필요한 자바의 다양한 중급 기능을 예제 코드로 깊이있게 학습합니다., 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을

www.inflearn.com

 

예외에 대한 내용을 정리한 게 있긴 한데 한번 더 정리해서 더 자세하게 예외 처리가 왜 필요한지?, 예외를 어떻게 처리하는 게 좋은 방법인지?를 알아보자.

 

다음과 같은 프로그램 구성도가 있다고 가정해보자.

 

프로그램의 흐름은 다음과 같다.

  1. 사용자가 데이터를 입력한다.
  2. 입력한 데이터를 NetworkService가 받아서 외부 서버에 전송한다.

여기서 외부 서버에 전송을 하려면, 외부 서버와 연결을 해주는 중간다리가 필요하다. 그 중간다리 역할을 NetworkClient가 해준다.

 

그래서 NetworkClient가 하는 역할은 다음과 같다.

  • 외부 서버와 연결한다.
  • 데이터를 외부 서버에 전송한다.
  • 외부 서버와 연결을 해제한다.

 

위 내용을 토대로 코드를 작성해보자.

NetworkClientV0

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 + " 서버 연결 해제");
    }
}
  • connect(): 외부 서버와 연결을 담당하는 메서드이다. 물론 실제로 연결하지 않지만 연결한다고 생각하자.
  • send(): 외부 서버에 데이터를 전송하는 메서드이다.
  • disconnect(): 외부 서버와의 연결을 해제하는 메서드이다.

NetworkServiceV0

package exception.ex0;

public class NetworkServiceV0 {

    public void sendMessage(String data) {
        String address = "https://example.com";
        NetworkClientV0 client = new NetworkClientV0(address);

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

서비스 클래스에선 NetworkClient를 사용한다. 그래서 접속할 외부 서버의 주소를 전달하고, 연결하고 데이터를 전송하고 연결을 해제하는 메서드를 순차적으로 호출한다.

 

Main

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.println("전송할 문자: ");
            String input = scanner.nextLine();
            if (input.equals("exit")) {
                break;
            }

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

    }
}

 

이제 사용자와 상호작용하는 부분이다. "exit" 문자열이 들어오면 루프를 빠져나와 프로그램을 종료한다. 

사용자가 입력한 문자열이 데이터가 되어 외부 서버에 전송이 될 것이다.

 

실행결과:

전송할 문자: 
Hello
https://example.com 서버 연결 성공
https://example.com 서버에 데이터 전송: Hello
https://example.com 서버 연결 해제

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

 

나의 코드가 원하는 대로 동작을 한다. 근데 원하는 대로 동작 안한다면? 세상에 아름다운 이야기만 있지는 않다.

예를 들어, 외부 서버와의 연결이 실패한다면? 연결은 성공했지만 데이터를 전송하는 과정에 어떤 문제로 인해 데이터를 전송하는데 실패한다면? 그런 상황을 생각해보기 위해 위 코드를 한번 수정해보자.

 

NetworkClientV1

package exception.ex1;

public class NetworkClientV1 {

    private final String address;
    private boolean connectError;
    private 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;
        }
    }
}

 

이제 이 NetworkClientV1 클래스에는 connectError, sendError 두 개의 필드가 있다. 그리고 각 필드값이 true라면, connect() 메서드나 send() 메서드에서 에러 상황이 일어났다고 가정한다.

 

그리고 boolean 타입의 필드 기본값은 false이니까, 들어오는 데이터에 따라 그 값을 true로 변경하는 initError() 메서드를 만든다.

메서드 내용은 전달받은 파라미터 data(사용자 입력값)의 값이 "error1"을 포함하면 connectErrortrue로, "error2"를 포함하면 sendErrortrue로 변경한다.

 

NetworkServiceV1_1

package exception.ex1;


public class NetworkServiceV1_1 {

    public void sendMessage(String data) {
        String address = "https://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");
    }
}

이제 NetworkClientV1을 가져다가 사용하는 서비스를 보자.

우선 사용자로부터 입력받은 data에 대한 initError() 메서드를 먼저 실행한 후, connect(), send() 메서드를 각각 실행한다.

 

근데 connect() 또는 send() 메서드가 에러가 있는 경우 더이상 "Success"를 반환하지 않고 "connectError" 또는 "sendError"를 반환하기 때문에 그럴 경우 에러가 발생했다는 것을 알려주고 다음 단계를 중지해야 한다. 그도 그럴것이 앞 단계에서 에러가 났는데 다음 단계가 잘 될리 없다.

 

실행결과:

전송할 문자: 
hello
https://example.com 서버 연결 성공
https://example.com 서버에 데이터 전송: hello
https://example.com 서버 연결 해제

전송할 문자: 
error1
https://example.com 서버 연결 실패
[네트워크 오류 발생] 오류 코드: connectError

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

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

 

이제 만약 에러가 발생하면 에러가 발생했다는 사실을 사용자에게 출력해주고, 다음 단계를 진행하지 않는다. 좋다. 근데 한가지 문제가 있다. 에러가 발생하면 자원을 반납하는 disconnect() 메서드가 호출되지 않고 있다. 

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

 

그럼, 이제 사용자에게 에러가 발생했다는 것도 알려주고, 에러가 나면 다음 단계를 진행하지 않는것 까지는 좋은데 에러가 발생하든 발생하지 않든 disconnect() 메서드는 호출되어야 하기 때문에 이 부분을 해결해야 한다.

 

NetworkServiceV1_3

package exception.ex1;


public class NetworkServiceV1_3 {

    public void sendMessage(String data) {
        String address = "https://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 static boolean isError(String connectResult) {
        return !connectResult.equals("Success");
    }
}

해결하는 방법은 간단하다. if-else로 조건을 분기하고 에러가 발생해도 바로 return을 하지 않으면 된다.

이렇게 코드를 수정하면 연결에 실패하든 데이터 전송에 실패하든 disconnect() 메서드를 호출할 수 있다.

 

실행결과:

전송할 문자: 
hello
https://example.com 서버 연결 성공
https://example.com 서버에 데이터 전송: hello
https://example.com 서버 연결 해제

전송할 문자: 
error1
https://example.com 서버 연결 실패
[네트워크 오류 발생] 오류 코드: connectError
https://example.com 서버 연결 해제

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

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

 

이제 정상흐름일 땐 정상흐름답게, 예외 흐름일 땐 문제 없이 사용자에게 예외 발생을 알리고 disconnect() 메서드도 잘 호출한다. 

 

근데, 아직 해결되지 않은 문제가 있다. 

정상 흐름과 예외 흐름이 섞여있어서 코드 해석이 어려워졌고 예외 처리 흐름이 심지어 더 많은 코드 분량을 차지하고 있기 때문에 어디가 정상 흐름이고 어디가 예외 흐름인지 분석하기가 어렵다. 

그래서 이렇게 connect(), send() 메서드의 반환값을 통해서 예외 상황을 처리하는 방식은 해결할 수 없는 문제를 가지고 있다.

 

이런 문제를 해결하기 위해 바로 예외 처리 메커니즘이 존재한다. 예외 처리를 사용하면 정상 흐름과 예외 흐름을 명확하게 분리할 수 있다.

 

결론

자바에서 제공하는 예외 처리 메커니즘을 사용해야 하는 이유는 다음과 같다.

  • 세상엔 정상 흐름만 존재하지 않는다.
  • 예외 흐름을 반환값 같은 코드적으로 해결하려 해도 예외 흐름과 정상 흐름이 섞여 있어 코드를 분석하기 어렵다.
  • 코드적으로 해결하다보니 예외 흐름이 정상 흐름보다 코드 양이 더 많아진다.

 

728x90
반응형
LIST

'JAVA의 가장 기본이 되는 내용' 카테고리의 다른 글

예외 처리 3 (예외 처리 도입)  (0) 2024.04.23
예외 처리 2 (예외 계층)  (0) 2024.04.23
try-with-resources  (0) 2024.04.23
익명 클래스  (0) 2024.04.21
지역 클래스  (0) 2024.04.21