예외 처리 (3)

2024. 12. 11. 17:44·Java

 

예외 계층

 

예외를 세분화 해보자.

 

 

우리는 이전에 NetworkClientException 예외 클래스 하나로만 예외를 관리했다. 단순히 오류 코드로 예외를 분류 한 것이다.

예외를 계층화 시켜서 다양하게 만들면 더 세밀하게 예외를 처리할 수 있다.

package exception.ex3.exception;

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;
    }
}

 

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

 

package exception.ex3.exception;

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;
    }
}

 

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

 

 

package exception.ex3.exception;

public class NetworkClientV3 {

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

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

    public void connect() throws ConnectExceptionV3 {
        if (connectError) {
            throw new ConnectExceptionV3(address, address + " 서버연결 실패");
        }
        System.out.println(address + "서버 연결 성공");
    }

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

 

connect() , send() 를 보면 이전처럼 throws NetworkClientException가 아닌 각 예외에 맞는 예외를 던지고 있다.

각각의 예외 클래스엔 getAddress(), getSendData()와 같이 고유의 기능을 활용해 예외를 세분화 할 수 있다.

package exception.ex3.exception;

public class NetworkServiceV3_1 {

    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 (SendExceptionV3 e) {
            System.out.println("[전송 오류] 주소: " + e.getSendData() + ", 메세지: " + e.getMessage());
        } finally {
            client.disconnect();
        }
    }
}

오류 메세지가 다르게 출력

 

예외 계층 활용

만약, NetworkClientException에서 수 많은 예외를 발생시킨다고 가정했을 때, 모든 예외를 하나하나 잡아야 한다면...

너무나도 번거로울 것이다. 그래서 예외에 우선순위를 두고 예외 계층을 활용하는 방법이 있다.

 

연결 오류는 중요하기 때문에 ConnectException을 명확히 남기도록 하고, 나머지 예외는 단순하게 출력하는 방법이다.

 

package exception.ex3.exception;

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();
        }
    }
}

 

연결 오류시에만 자세한 에러코드를 남기고, 전송 실패 혹은 다른 예외(NetworkClientException 자식의 예외)가 발생한다면

네크워크 오류라는 에러 코드를 남기는 것이다. 그 이외의 예외 (런타임 등)이 발생하면 알 수 없는 오류로 처리된다.

 

catch는 순서대로 작동한다. 따라서 디테일한 자식의 예외를 먼저 처리해야한다.

부모는 자식을 담을 수 있다. 따라서 순서가 바뀐다면 ConnectException의 연결오류가 아닌 네트워크 오류 코드가 나올것이다.

물론 컴파일러가 잡아주니 크게 문제되진 않는다.

 

예외를 한번에 잡는 기능도 있다.

try {
    client.connect();
    client.send(data);
} catch (ConnectExceptionV3 | SendExceptionV3 e) {
    System.out.println("[연결 또는 전송 오류] 주소: , 메세지: " + e.getMessage());

 

위 처럼 connect 또는 send 예외를 한번에 잡을 수 있다. 하지만 각 예외의 고유의 메서드는 사용할 수 없다. 

(getAddress() 혹은 getSendData()는 사용할 수 없음)

공통부모의 기능 getMessage()만 사용이 가능하다.

 

 

실무에서의 예외 처리방안

예외도 처리할 수 없는 예외가 있다. 네트워크 서버, DB 서버 등의 문제가 발생했을 때 개발자가 예외처리로 해결 할 수 없다.

DB서버가 다운됬는데 try catch로 잡을 수 있을리 없다. 예외를 처리해도 어차피 접속 실패 오류가 뜰 것이다.

이런 경우에 웹의 경우 에러 페이지를 띄우고, 내부 개발자가 빠르게 인지하도록 에러 로그를 남겨야 한다.

 

 

체크 예외 사용 시나리오

 

체크예외는 컴파일러가 체크해주기 때문에 오래전부터 많이 사용되었지만, 현대의 프로그램들이 점점 복잡해지면서

체크 예외를 사용하기에 부담이 된다. 일일이 명시해줘야 하는 양이 많아졌기 때문이다.

 

 

위 이미지에는 3가지 이지만 10가지가 넘는 예외를 다 처리해야 한다고 생각해보자...........

try-catch-catch-catch-catch { .... } 언제 다 할까?

 

서비스에서 처리하지 못하는 예외는 throws로 던져야한다. 이 또한 굉장히 복잡해진다.

throws NetworkException, DatabseException, XxxException, { .... }

Main과 Service만 있다면 그나마 낫지만, 중간에 다른 클래스가 있다면 똑같은짓을 여러번 반복해야한다.

 

그러면 throws Exception으로 한번에 해결하면 되는게 아닌가?

- 안된다. Exception은 최상위 타입이므로 모든 체크예외를 다 밖으로 던져버리기 때문에 중요한 체크 예외를 다 놓치게 된다.

 

정리해보면 그냥 답이 안나오는 상황이다. 

 

 

 

언체크 예외 사용 시나리오

 

이번에는 런타임 예외를 전달한다고 가정해보자.


런타임 예외는 throws를 생략해도 되므로 체크 예외보다 코드 작성이 훨씬 수월하다.

그리고 런타임 예외라고 해서 catch로 예외를 잡지 못하는게 아니다. try-catch로 예외를 잡을 수 있다.

어차피 서버문제가 발생하면 service에서 해결할 수 있는 예외처리 방법은 없다.

 

이렇게 처리할 수 없는 예외를 다 던져버리면 어떻게 해결하는가?

예외를 중간에 여러곳에서 나눠 처리하기 보다 예외를 공통으로 처리할 수 있는 곳을 만들어서 한곳에서 해결하면 된다.

어차피 해결할 수 없는 예외들이기 때문에 고객에게 에러 페이지를 띄우고, 내부 개발자가 인지하고 해결할 수 있도록

로그를 남겨두면 된다. (메일을 보낸다던지, 슬랙으로 알림을 보낸다던지)

 

공통 예외처리 구현

 

이제 NetworkClientExceptionV4는 RuntimeException을 상속받는다.

 

package exception.ex4;

import exception.ex4.exception.ConnectExceptionV4;
import exception.ex4.exception.SendExceptionV4;

public class NetworkClientV4 {

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

    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);
        }
        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;
        }
    }
}

 

이제 connect(), send()는 언체크 예외이므로 throws 를 사용하지 않는다.

 

package exception.ex4;

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();
        }
    }
}

 

connect, send 에러를 잡아도 해당 오류는 개발자 입장에서 복구할 수 없다. 따라서 예외를 밖으로 던진다.

해결할 수 없는 예외를 다 던졌기 때문에 service의 코드가 더욱 간결해 졌다. 

 

package exception.ex4;

import exception.ex4.exception.SendExceptionV4;

import java.util.Scanner;

public class MainV4 {

    public static void main(String[] args) {

        NetworkServiceV4 networkService = new NetworkServiceV4();

        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("전송할 문자: ");
            String input = scanner.nextLine();
            if (input.equals("exit")) {
                break;
            }

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

            System.out.println();
        }

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

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

 

공통으로 처리하는 부분이 생겼다. Exception을 잡아서 지금까지 해결하지 못한 예외를 여기서 공통으로 처리한다.

예외도 객체이므로 공통 처리 메서드인 exceptionHandler(e)에 예외 객체를 전달한다.

 

exceptionHandler에 스택 트레이스를 출력하는 메서드를 사용했다. System.out 보다는 System.err 를 사용하자

(사실 직접 호출하면 콘솔에서 밖에 출력이 안되므로 크게 도움되진 않음. 문법상 에러 코드를 보기 위함임)

 

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

 

예외도 객체이므로 instanceof를 사용해 추가적인 처리를 할 수 있다. sendError에 추가적인 오류코드를 출력하도록 했다.

 

 

 

try-with-resources

우리는 연결을 해제하기위해 finally에 client.disconnect()를 사용했다. 그런데 그보다 더 이전에 리소스를 반납할 수 있다.

 

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

public class NetworkClientV5 implements AutoCloseable {

   ...
   ...
   ...
   
    @Override
    public void close() {   // 이 메서드는 예외를 던지지 않으므로 throws를 삭제했음
        System.out.println("NetworkClientV5.close");
        disconnect();
    }
}

 

 

 

package exception.ex4;

public class NetworkServiceV5 {

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

        try (NetworkClientV5 client = new NetworkClientV5(address)) {
            client.initError(data); // 추가
            client.connect();
            client.send(data);
            // catch 구문은 작성하지 않아도 됨. 순서를 확인하기 위해 작성한 코드임
        } catch (Exception e) {
            System.out.println("예외 확인" + e.getMessage());
            throw e;
        }
    }
}

 

try 괄호 안에 자원을 명시해서 사용했다. 이 자원이 끝나면 AutoCloseable.close()를 호출해서 자원을 해제한다.

 

 

try가 끝나는 시점에서 close() 메서드가 호출된 것을 알 수 있다.

 

Try with resources의 장점

1. 리소스 누수 방지 : 모든 리소스가 제대로 닫히도록 보장한다. 실수로 finally를 사용하지 않아도 작동한다.

2. 코드 간결성 및 가독성 향상 : 로직에 close() 호출이 명시적으로 작성되지 않아 코드가 간결해진다.

3. 스코프 범위 한정 : 위 코드에선 리소스로 사용되는 client 변수의 스코프가 try 안에서만 작동한다. 따라서 유지보수성이 좋아진다.

4. 조금 더 빠른 자원해제 : finally에선 제일 마지막에 자원을 해제했지만 그보다 일찍 자원을 해제함으로써 자원낭비를 조금이나마 줄일 수 있다.

'Java' 카테고리의 다른 글

제네릭 (2)  (1) 2024.12.15
제네릭 (1)  (1) 2024.12.12
예외 처리 (2)  (0) 2024.12.10
예외 처리 (1)  (0) 2024.12.10
중첩 클래스, 내부 클래스 (Local, Anonymous)  (0) 2024.11.19
'Java' 카테고리의 다른 글
  • 제네릭 (2)
  • 제네릭 (1)
  • 예외 처리 (2)
  • 예외 처리 (1)
공부처음하는사람
공부처음하는사람
  • 공부처음하는사람
    lazzzykim
    공부처음하는사람
  • 전체
    오늘
    어제
    • 분류 전체보기 (127)
      • Kotlin (31)
      • Java (55)
      • Spring (18)
      • Algorithm (3)
      • TroubleShooting (1)
      • 내일배움캠프 프로젝트 (14)
      • Setting (2)
      • ... (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • 링크

  • 인기 글

  • 태그

    빈 생명주기
    제네릭
    OCP
    언체크예외
    배열
    생성자 주입
    김영한의 실전자바
    중첩클래스
    java
    kotlin
    spring
    다형성
    싱글톤
    김영한
    캡슐화
    김영한의 실전 자바
    내일배움캠프
    @Component
    Di
    래퍼클래스
  • hELLO· Designed By정상우.v4.10.3
공부처음하는사람
예외 처리 (3)
상단으로

티스토리툴바