공부/Java

자바 예외처리 2

Stair 2024. 9. 16. 12:46
반응형

https://surrealcode.tistory.com/65

 

자바 예외처리 1

예외처리는 정상 흐름과 예외 흐름을 명확하게 분리하여 가독성을 높이는 데 있다.  우선 다음의 예제를 살펴보자. 이 예제는 서버와 통신(을 한다고 가정한다)하여 메세지를 보내는 예제이다.

surrealcode.tistory.com

예외처리에 관한 1편은 이전 내용을 참고하는 것이 좋다.

이전에 사용했던 소스 코드를 여기서 재사용할 예정이다.

 

 

앞서 만든 통신 프로그램은 반환값을 사용하여 예외를 처리했었다. 이런 경우 다음과 같은 문제가 있다

1. 정상 흐름과 예외흐름이 섞여있어 코드를 이해하기가 어렵다.

2. 예외 흐름이 더 많은 코드 분량을 차지한다.

 

여기에 예외처리를 도입해서 처리해보자.

public class NetworkClientExceptionV2 extends Exception {

    private String errorCode;

    public NetworkClientExceptionV2(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() {
        return errorCode;
    }
}
public class NetworkClientV2 {

    public boolean connectError;
    public boolean sendError;

    private final String address;

    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;
        }
    }
}
public class NetworkServiceV2_1 {

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

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

    }
}
public class MainV2 {

    public static void main(String[] args) throws NetworkClientExceptionV2 {
//        NetworkServiceV1_1 networkService = new NetworkServiceV1_1();
//        NetworkServiceV1_2 networkService = new NetworkServiceV1_2();
        NetworkServiceV2_1 networkService = new NetworkServiceV2_1();


        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("프로그램을 정상 종료합니다.");


    }
}

 

우선 catch 없이 전부 밖으로 던지는 코드를 생성해보았다. 

 error1과 error2를 입력 시 예외를 잡지 않았기때문에 main()밖으로 예외가 던져지면서 스택 트레이스를 출력하고 프로그램을 종료한다.

 

여기서 남은 문제는 다음과 같다.

1. 예외 처리를 도입했지만, 아직 예외가 복구되지 않는다. 따라서 예외가 발생하면 프로그램이 종료된다.

2. 사용 후에는 반드시  disconnect()를 호출해서 연결을 해제해야 한다.

public class NetworkServiceV2_2 {

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

        try {
            client.connect();
        } catch (NetworkClientExceptionV2 e) {
            System.out.println("[오류] 코드: " + e.getErrorCode()
                    + ", 메시지: " + e.getMessage());
            return;
        }
        try {
            client.send(data);
        } catch (NetworkClientExceptionV2 e) {
            System.out.println("[오류] 코드: " + e.getErrorCode()
                    + ", 메시지: " + e.getMessage());
            return;
        }
        client.disconnect();

    }
}

connect(),send() 와 같이 예외가 발생할 수  있는 곳을 try~catch를 사용해서 NetworkClientExceptionV2 예외를 잡았다.

여기서는 예외를 잡으면 오류 코드와 예외 메시지를 출력한다.

예외를 잡아서 처리했기 때문에 이후에는 정상 흐름으로 복귀한다. 여기서 리턴을 사용해서 sendMessage()메서드를 정상적으로 빠져나간다.

 

아직 disconnect()를 호출해서 연결을 해제해야 하는 문제와

정상 흐름, 예외 흐름을 분리하는 작업이 남아있다.

public class NetworkServiceV2_3 {

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

        try {
            client.connect();
            client.send(data);
            client.disconnect();
        } catch (NetworkClientExceptionV2 e) {
            System.out.println("[오류] 코드: " + e.getErrorCode()
                    + ", 메시지: " + e.getMessage());
            return;
        }


    }
}

하나의 try 안에 정상 흐름을 모두 담는다.

그리고 예외 부분은 catch에서 처리하도록 한다.

자바의 예외 처리 메커니즘과 try, catch 구조 덕분에 정상 흐름은 try블럭에 모아서 처리하고, 예외 흐름은 catch블럭에 별도로 모아서 처리할 수 있었다. 덕분에 코드를 읽기 좀 더 쉬워졌다.

 

마지막으로 예외처리를 한 이후에  disconnect()를 호출해서 연결을 해제해야 한다.

 

앞서 말했든 외부 연결과 같은 자바 외부의 자원은 자동으로 해제가 되지 않는다. 따라서 외부 자원을 사용한 후에는 연결을 해제해서 외부 자원을 반드시 반납해야 한다.

예외가 발생해도 disconnect()를 반드시 호출해서 연결을 해제하고 자원을 반납하려면 어떻게 해야할까?

public class NetworkServiceV2_4 {

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

        try {
            client.connect();
            client.send(data);
        } catch (NetworkClientExceptionV2 e) {
            System.out.println("[오류] 코드: " + e.getErrorCode()
                    + ", 메시지: " + e.getMessage());
        }

        client.disconnect();


    }
}

 

가장 직관적으로 생각나는 것은 위처럼 client.disconnect()를 밖으로 빼내서 항상 disconnect를 호출하는 서비스를 만드는 것이다. 하지만 지금과 같은 방식에는 catch에서 잡을 수 없는 예외가 발생할 때 문제가 생긴다.

    public void send(String data) throws NetworkClientExceptionV2 {
        if(sendError){
//            throw new NetworkClientExceptionV2("sendError: ",
//                    address + "서버에 데이터 전송 실패: " + data);
            //중간에 다른 예외가 발생
            throw new RuntimeException("ex");
        }
        System.out.println(address + " 서버에 데이터 전송: " + data);
    }

체크 예외가 아닌 언체크 예외가 발생했다고 가정해보자.

 

전송할 문자: error2
http://example.com 서버 연결 성공
Exception in thread "main" java.lang.RuntimeException: ex
at exception.ex2.NetworkClientV2.send(NetworkClientV2.java:29)
at exception.ex2.NetworkServiceV2_4.sendMessage(NetworkServiceV2_4.java:13)
at exception.ex2.MainV2.main(MainV2.java:25)

 

RuntimeException은 언체크 예외이고 현재 catch엔 체크예외만 잡고있기 때문에 throw가 없어도 에러를 밖으로 던져버린다.(언체크는 throw없어도 던진다) 밖에서도 언체크를 잡는 catch가 없기에 밖으로 던지고, main마저 밖으로 던져서 스택트레이스가 터진다.

 

사용 후에 반드시 disconnect()를 호출해서 연결 해제를 보장하는 것은 쉽지 않다. 왜냐하면 정상적인 상황, 예외 상황 그리고 어디선가 모르는 예외를 밖으로 던지는 상황까지 모든 것을 고려해야 한다. 하지만 앞서 보았듯이 지금과 같은 구조로는 항상 disconnect()와 같은 코드를 호출하는 것은 매우 어렵고 실수로 놓칠 가능성이 높다.

 

이 문제를 해결하기 위해 자바는 어떤 경우라도 반드시 호출되는 finally 라는 기능을 제공한다.

try~catch~finally

구조는 정상 흐름, 예외 흐름, 마무리 흐름을 제공한다.

여기서  try를 시작하기만 하면, finally 코드 블럭은 어떤 경우라도 반드시 호출된다.

심지어 try, catch안에서 잡을 수 없는 예외가 발생해도 finally는 반드시 호출된다.

**예외가 생겨 밖으로 던지게 되더라도 finally 코드 블럭이 끝나고 난 이후 예외가 밖으로 던져진다.

public class NetworkServiceV2_5 {

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

        try {
            client.connect();
            client.send(data);
        } catch (NetworkClientExceptionV2 e) {
            System.out.println("[오류] 코드: " + e.getErrorCode()
                    + ", 메시지: " + e.getMessage());
        }finally {
            client.disconnect();
        }



    }
}

 

만약 RuntimeException이 발생되어도

전송할 문자: error2
http://example.com 서버 연결 성공
http://example.com 서버 연결 해제
Exception in thread "main" java.lang.RuntimeException: ex
at exception.ex2.NetworkClientV2.send(NetworkClientV2.java:29)
at exception.ex2.NetworkServiceV2_5.sendMessage(NetworkServiceV2_5.java:13)
at exception.ex2.MainV2.main(MainV2.java:26)

 

서버에 대한 연결은 해제를 하고 RuntimeException이 발생하게 된다(예외가 생겨 밖으로 던지게 되더라도 finally를 먼저 실행 후에 밖으로 던져지기 때문에)

 

 

 

예외 계층

예외를 단순히 오류 코드로 분류하는 것이 아니라, 예외를 계층화 해서 다양하게 만들면 더 세밀하게 예외를 처리할 수 있다.

NetworkClientExceptionV3 : NetworkClient에서 발생하는 모든 예외는 이 예외의 자식이다.

ConnectExceptionV3 : 연결 실패시 발생하는 예외이다. 내부에 연결을 시도한  address를 보관한다.

SendExceptionV3 : 전송 실패시 발생하는 예외이다. 내부에 전송을 시도한 데이터인 sendData를 보관한다.

 

자바에서 예외는 객체이다. 따라서 부모 예외를 잡거나 던지면, 그 자식 예외도 함께 잡거나 던질 수 있다.

특정 예외를 잡아서 처리하고 싶다면, 자식 하위 예외를 잡아서 처리하면 된다.

 

public class NetworkClientExceptionV3 extends Exception{

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

}
public class ConnectExceptionV3 extends NetworkClientExceptionV3{
    private final String address;

    public ConnectExceptionV3(String address, String message) {
        super(message);
        this.address = address;
    }

    public String getAddress() {
        return address;
    }
}
public class SendExceptionV3 extends NetworkClientExceptionV3{
    private final String sendData;

    public SendExceptionV3(String sendData, String message) {
        super(message);
        this.sendData = sendData;
    }

    public String getSendData() {
        return sendData;
    }
}

 

NetworkClientExceptionV3를 상속받는 ConnectExceptionV3와 SendExceptionV3를 구현했다.

try {
    client.connect();
    client.send(data);
} catch (ConnectExceptionV3 e) {
    System.out.println("[연결 오류] 주소 : " + e.getAddress() +
            ", 메시지" + e.getMessage());
} catch (SendExceptionV3 e) {
    System.out.println("[전송 오류] 전송 데이터: " + e.getSendData() +
            ", 메시지" + e.getMessage());
} finally {
    client.disconnect();
}

이를 통해 예외 처리를 좀 더 세분화 시킬 수 있었다.

 

public class NetworkServiceV3_2 {

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

        try {
            client.connect();
            client.send(data);
        } catch (ConnectExceptionV3 e) {
            System.out.println("[연결 오류] 주소 : " + e.getAddress() +
                    ", 메시지" + e.getMessage());
        } catch (NetworkClientExceptionV3 e) {
            System.out.println("[네트워크 오류] 메시지 : " + e.getMessage());
        } catch (Exception e){
            System.out.println("[알 수 없는 오류] 메시지 : " + e.getMessage());
        } finally {
            client.disconnect();
        }



    }
}

 또는 위와 같이 연결 오류만 중요하게 생각하여 연결 오류만 세분하 하여 잡고 나머지는 네트워크 오류로 잡을수도 있다. 언체크같은 runtimeException은 Exception 객체(얘가 부모)를 사용하여 알 수 없는 오류로 퉁 쳐서 잡았다.

 

 

 

 

 

실무에서 사용되는 예외 처리

처리할 수 없는 예외 : 상대 네트워크 서버에 문제가 발생해서 통신이 불가능 하거나, 데이터베이스 서버에 문제가 발생해서 접속이 안되면, 애플리케이션에서 연결 오류, 데이터베이스 접속 실패와 같은 예외가 발생한다.

이렇게 시스템 오류 때문에 발생한 예외들은 대부분 예외를 잡아도 해결할 수 있는 것이 거의 없다.

이런 경우 고객에게는 "현재 시스템에 문제가 있습니다"라는 오류 메시지를 보ㅇ여주고, 만약 웹이라면 오류 페이지를 보여주면 된다. 그리고 내부 개발자가 문제 상황을 빠르게 인지할 수 있도록, 오류에 대한 로그를 남겨두어야 한다.

 

체크예외의 부담 : 체크 예외는 개발자가 실수로 놓칠 수 있는 예외들을 컴파일러가 체크해주기 때문에 오래전부터 많이 사용되었다. 그런데 앞서 설명한 것 처럼 처리할 수 없는 예외가 많아지고, 또 프로그램이 점점 복잡해지면서 체크 예외를 사용하는 것이 점점 더 부담스러워졌다.

 

 

 

부담이 가중된 개발자는 throws Exception을 써서 모든 예외를 다 던져버리는 최악의 수를 놓게 된다.

Exception은 애플리케이션에서 일반적으로 다루는 모든 예외의 부모이다. 따라서 이렇게 한 줄만 넣으면 모든 예외를 다 던질 수 있다.

이렇게 하면 Exception은 물론이고 그 하위 타입인 NetworkException, DatabaseException 도 함께 던지게 된다. 그리고 이후에 예외가 추가되더라도 throws Exception은 변경하지 않고 그대로 유지할 수가 있다. 코드가 깔끔해지는 것 같지만 이 방법에는 치명적인 문제가 있다.

 

throws Exception의 문제 : Exception은 최상위 타입이므로 모든 체크 예외를 다 밖으로 던지는 문제가 발생한다.

결과적으로 체크 예외를 체크할 수 있는 기능이 무효화 되고, 중요한 체크 예외를 다 놓치게 도니다. 중각에 중요한 체크 예외가 발생해도 Exception을 던져버리기 때문에 문법에 맞다고 판단해서 컴파일 오류가 발생하지 않는다.

 

문제 정리

1. 처리할 수 없는 예외 : 예외를 잡아서 복구할 수 있는 예외보다 복구할 수 없는 예외가 더 많다

2. 체크 예외의 부담 : 처리할 수 없는 예외는 밖으로 던져야 한다. 체크 예외이므로 throws에 던질 대상을 일일히 명시해야 한다.

 

 

본인이 해결할 수 있는 예외만 잡아서 처리하고, 본인이 해결할 수 없는 예외는 신경쓰지 않는 것이 더 나은 선택일수도 있다.

 

 

언체크 예외를 던질때

class Service{

    void sendMessage(Stringi data){

    }

}

1. 언체크 예외이므로 throws를 선언하지 않아도 된다.

2. 사용하는 라이브러리가 늘어나서 언체크 예외가 늘어도 본인이 필요한 예외만 잡으면 되고, throws를 늘리지 않아도 된다.

 

일부 언체크 예외를 잡아서 처리할때

try{

} catch(XXXException){~}

1. 앞서 말했듯 상대 서버가 내려갔거나, DB에 문제가 발생한 경우 Service에서 예외를 잡아도 복구할 수 없다.

2.  Service에서는 어짜피 본인이 처리할 수 없는 예외들이기 때문에 밖으로 던지는 것이 더 나은 결정이다.

3. 언체크 예외는 잡지 않으면 자동으로 밖으로 던진다

4. 만약 일부 언체크 예외를 잡아서 처리할 수 있다면 잡아서 처리하면 된다.

 

 

예외 공통 처리

처리할 수 없는 예외들은 예외를 공통으로 처리할 수 있는 곳을 만들어서 한곳에서 해결해버리면 된다.

어짜피 해결할 수 없는 예외이기에 오류 메세지를 보여주거나, 오류 페이지를 보여주면 된다. 그리고 내부 개발자가 지금의 문제 상황을 빠르게 인지할 수 있도록 오류에 대한 로그를 남겨두면 된다.

public class NetworkClientExceptionV4 extends RuntimeException{

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

}
public class SendExceptionV4 extends NetworkClientExceptionV4 {
    private final String sendData;

    public SendExceptionV4(String sendData, String message) {
        super(message);
        this.sendData = sendData;
    }

    public String getSendData() {
        return sendData;
    }
}
public class ConnectExceptionV4 extends NetworkClientExceptionV4 {
    private final String address;

    public ConnectExceptionV4(String address, String message) {
        super(message);
        this.address = address;
    }

    public String getAddress() {
        return address;
    }
}
public class NetworkClientV4 {

    public boolean connectError;
    public boolean sendError;

    private final String address;

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

    public void connect() {

        if(connectError){
            throw new ConnectExceptionV4(address,
                    address+" 서버 연결 실패");
        }

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

    public void send(String data){
        if(sendError){
            throw new SendExceptionV4(data,
                    address + "서버에 데이터 전송 실패: " + data);
            //중간에 다른 예외가 발생
//            throw new RuntimeException("ex");
        }
        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;
        }
    }
}
public class NetworkServiceV4 {

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

        try {
            client.connect();
            client.send(data);
        } finally {
            client.disconnect();
        }



    }
}
public class MainV4 {

    public static void main(String[] args) {

        NetworkServiceV4 networkService = new NetworkServiceV4();

        Scanner scanner = new Scanner(System.in);

        while (true){
            System.out.print("전송할 문자: ");
            String input = scanner.nextLine();

            if(input.equals("exit")){
                break;
            }

            try{
                networkService.sendMessage(input);
            } catch (Exception e){
                exceptionHander(e);
            }

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


    }


    //공통 예외 처리
    private static void exceptionHander(Exception e) {
        System.out.println("사용자 메세지: 죄송합니다. 알 수 없는 문제가 발생했습니다.");
        System.out.println("==개발자용 디버깅 메시지==");
        e.printStackTrace(System.out); //스택 트레이스 출력
        //e.printStackTrace()

        //필요하면 예외 별로 별도의 추가 처리 가능
        if(e instanceof SendExceptionV4 sendEx){
            System.out.println("[전송 오류] 전송 데이터 : " + sendEx.getSendData());
        }


    }
}

 

exceptionHandler()

해결할 수 없는 예외가 발생하면 사용자에게는 시스템 내에 알 수 없는 문제가 발생했다고 알려준다.

사용자가 디테일한 오류까진 알 필요 없다. 개발자는 빨리 문제를 찾고 디버깅할 수 있도록 오류 메시지를 남겨두어야 한다.

예외도 객체이므로 필요하면 instanceof 와 같이 예외 객체의 타입을 확인해서 별도의 추가 처리를 할 수 있다.


e,printStackTrace()

예외메시지와 스택 트레이스를 출력할 수 있다.

이 기능을 사용하면 예외가 발생한 지점을 역으로 추적할 수 있다.

e.printStackTrace()를 사용하면 System.err이라는 표준 오류에 결과를 출력한다 IDE에서는 System.err로 출력하면 출력 결과를 빨간색으로 보여준다.

 

참고 : 실무에서는 콘솔을 통해 에러를 출력하기 보다. Slf4J, logback같은 별도의 로그 라이브러리를 사용해서 콘솔과 특정 파일에 함께 결과를 출력한다.

 

 

 

try-with-resources

애플리케이션에서 외부 자원을 사용하는 경우 반드시 외부 자원을 해제해야 한다.

따라서 finally 구문을 반드시 사용해야 한다.

 

try에서 외부 자원을 사용하고, try가 끝나면 외부 자원을 반납하는 패턴이 반복되면서 자바에서는 Try with resources라는 편의 기능을 자바 7에서 도입했다. 이름 그대로 try에서 자원을 함께 사용한다는 뜻이다. 여기서 자원은 try가 끝나면 반드시 종료해서 반납해야 하는 외부 자원을 뜻한다.

 

이 기능을 사용하려면  AutoCloseable 인터페이스를 구현해야 한다.

 

package java.lang;

 

public interface AutoCloseable{

    void close() throws Exception;

}

이 인터페이스를 구현하면 Try with resources를 사용할 때 try가 끝나는 시점에 close()가 자동으로 호출된다.

 

그리고 다음과 같이 Try with resources구문을 사용하면 된다

try(Resource resource = new Resource()){}

 

 

 

반응형

'공부 > Java' 카테고리의 다른 글

자바 불변 객체 복습하기  (1) 2024.09.18
자바 Object 클래스 복습하기  (1) 2024.09.18
자바 예외처리 1  (0) 2024.09.15
자바 익명 클래스  (1) 2024.09.14
자바 지역 클래스  (0) 2024.09.14