Home > Java-Ecosystem > Java > 자바 예외 기본

자바 예외 기본
Java

예외 계층

java exception hierarchy

  • Object: 모든 객체의 최상위 부모
  • Throwable: 최상위 예외, 잡으면 안됨 (Error까지 잡히므로)
    • Error
      • 애플리케이션에서 복구 불가능한 시스템 예외 (메모리 부족, 심각한 시스템 오류…)
      • 애플리케이션 개발자는 이 예외를 잡지 않아야 함
      • 언체크 예외
    • Exception: 체크 예외 (런타임 예외 제외), 애플리케이션에서 개발자가 잡아야 할 실질적최상위 예외
      • RuntimeException: 언체크 예외 (=런타임 예외)

체크예외 VS 언체크 예외

  • 핵심
    • 언체크 예외는 throws 선언하지 않고 생략 가능 (자동 예외 던지기)
    • 나머지는 동일
  • 체크 예외
    • 컴파일러가 체크하는 예외
    • 체크 예외의 장단점
      • 예외를 누락하지 않도록 컴파일러가 안전 장치 역할 (누락 시 컴파일 오류)
      • 크게 신경쓰지 않고 싶은 예외까지 모두 반드시 잡거나 던져야 함
  • 언체크 예외
    • 컴파일러가 체크하지 않는 예외
    • 중요 예외의 경우 throws를 선언해두면 IDE를 통해 개발자가 편리하게 인지 가능 (보통은 생략)
    • 언체크 예외의 장단점
      • 신경쓰고 싶지 않은 언체크 예외 무시 가능
      • 개발자가 실수로 예외 누락 가능

예외 처리 기본

  • 기본 규칙
    1. 예외는 잡아서 처리하거나 던져야 한다
      • 예외를 잡는 코드: catch
      • 예외를 던지는 코드: throws
    2. 예외를 잡거나 던질 때 지정한 예외 뿐만 아니라 그 예외의 자식들도 함께 처리된다
  • 기본적으로 언체크(런타임) 예외를 사용하자
    • 체크 예외들은 바깥으로 던져야 하는데 이 과정에서 의존 관계 문제 발생
      • 실무에서 발생하는 대부분의 예외는 복구 불가능한 시스템 예외 (애플리케이션 단에서 처리 불가)
      • 의존 관계 문제
        • 컨트롤러, 서비스는 본인이 처리할 수 없어도 throws를 선언해 예외를 던져야 함
        • 컨트롤러, 서비스가 해당 체크 예외에 의존하게 되어 구현 기술 변경 시 OCP 위반
          • 예를 들어, DB 접근 기술을 변경한다면 예외를 포함한 컨트롤러, 서비스 코드를 수정
        • 수 많은 체크 예외를 일일이 명시해 던지는 것도 부담
    • 런타임 예외를 사용하면 처리할 수 없는 예외를 별도 선언 없이 그냥 두면 됨
      • 의존성 발생 X -> 기술 변경이 있어도 컨트롤러, 서비스 코드 변경 X -> OCP 준수
      • 대부분의 최근 라이브러리는 런타임 예외를 기본으로 제공 (스프링, JPA…)
      • 런타임 예외는 놓칠 수 있기 때문에 문서화가 중요 (혹은 명시적으로 코드에 throws 남기기)
  • 처리할 수 없는 예외들은 한 곳에서 공통처리
    • 서블릿 오류 페이지, 스프링 MVC ControllerAdvice 예외 공통 처리
      • 고객: 오류 페이지
      • 내부 개발자: 별도 로그, 슬랙, 문자, 메일을 통해 개발자가 빠르게 인지
    • API는 상태코드 500 응답
    • 예시 코드
        public class Main {
            public static void main(String[] args) {
                NetworkService networkService = new NetworkService();
      				
                try {
                    networkService.sendMessage();
                } catch (Exception e) { // 모든 예외를 잡아서 처리 
                    exceptionHandler(e);
                }
            }
      		
            //공통 예외 처리
            private static void exceptionHandler(Exception e) {
                //공통 처리
                System.out.println("사용자 메시지: 죄송합니다. 알 수 없는 문제가 발생했습니다.");
                System.out.println("==개발자용 디버깅 메시지==");
                e.printStackTrace(System.out); // 스택 트레이스 출력 
                //e.printStackTrace(); // System.err에 스택 트레이스 출력
                //실무에서는 보통 Slf4j 사용해 로그를 콘솔 출력 + 파일로 저장
                //e.printStackTrace는 콘솔에만 출력하므로 사용 X
      		
                //필요하면 예외 별로 별도의 추가 처리 가능
                if (e instanceof SendException sendEx) {
                    System.out.println("[전송 오류] 전송 데이터: " + sendEx.getSendData()); 
                }
            }
        }
      
  • 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용
    • 매우 심각한 문제는 개발자가 실수로 예외를 놓치면 안된다고 판단할 수 있음
    • 체크 예외 예시 (무조건 해야하는 건 아님)
      • 계좌 이체 실패 예외
      • 결제시 포인트 부족 예외
      • 로그인 ID, PW 불일치 예외
  • Exception을 던지지 말자
    • 코드가 깔끔해지는 것 같지만, 모든 체크 예외를 다 던져 버려서 중요한 체크 예외를 놓침
    • 꼭 필요한 경우가 아니면 Exception 자체를 밖으로 던지는 것은 좋은 방법이 아님
  • 스택 트레이스를 남기자
    • 로그 남기기
      • log.info("예외 처리, message={}", e.getMessage(), e);
      • 로그의 마지막 인수에 예외 객체 전달하면 로그에 스택 트레이스를 출력
    • 예외를 전환할 때는 반드시 기존 예외를 포함하자
      • throw new RuntimeSQLException(e); - 기존 예외 e 포함
      • 덕분에 기존 예외와 스택 트레이스까지 확인 가능
      • 포함하지 않으면 실제 DB에서 발생한 근본적인 원인을 확인할 수 없는 심각한 문제 발생

예외를 계속 던지면 벌어지는 상황

  • 자바 main() 쓰레드의 경우 예외 로그를 출력하면서 시스템이 종료
  • 웹 애플리케이션의 경우 WAS가 예외를 받아 처리하고 개발자가 지정한 오류 페이지 보여줌
    (예외 하나로 시스템이 종료되면 안됨)

사용자 예외 만들기

  • Exception을 상속 받으면 체크 예외
  • RuntimeException을 상속 받으면 언체크 예외
  • 오류 메시지 보관하기
    • 생성자를 통해 오류 메시지를 보관할 것 (예외가 제공하는 기본 기능)
    • super(message)로 전달한 메시지는 ThrowabledetailMessage에 보관됨
    • getMessage()로 조회 가능
        public class MyCheckedException extends Exception {
            public MyCheckedException(String message) {
                super(message);
            }
        }
      
  • 적당한 예외 계층화하기 (너무 많아도 문제)
    • 상속을 사용해 예외를 계층화하면 보다 세밀한 예외 처리 가능
      • e.g.
        • NetworkClientException (부모)
          • ConnectException (자식, 내부 연결 시도 address 보관)
          • SendException (자식, 내부 전송 시도 데이터 sendData 보관)
    • 각각의 하위 예외에 고유 기능을 만들어 활용 가능
      • e.g. e.getAddress(), e.getSendData()
    • 부모 예외를 잡아 자식까지 한 번에 처리하거나 특정 하위 예외만 잡아 처리 가능
      • 중요한 특정 하위 예외만 메시지를 명확히 남기고 나머지는 공통 처리 가능
        • e.g.
          • [연결 오류] 주소: ... (하위 예외)
          • [네트워크 오류] 메시지: ... (부모 예외)
          • [알 수 없는 오류] 메시지: ... (그 외 예외 공통 처리)

예외 처리 발전 과정 예시 (e.g. NetworkClient)

  • 반환 값(문자열)으로 예외 처리
    • 분기 처리 및 return으로 네트워크 연결 및 해제, 데이터 전송 관리 등이 가능
    • 가장 중요한 정상 흐름이 한눈에 들어오지 않음 (정상 흐름과 예외 흐름 분리 X, 가독성 감소)
  • 예외 처리 메커니즘 사용 (try ~ catch ~ finally)
    • 성공 여부를 반환값이 아닌 메서드 정상 종료 여부로 판단
    • 정상 흐름과 예외 흐름을 명확히 분리가독성 상승 (try 블록, catch 블록)
    • 반드시 실행되어야 하는 코드안전하게 호출하도록 보장 (finally)
      • finally가 없으면 catch에서 잡지 못한 예외가 발생할 때 문제가 생김
      • 외부 자원 해제 등에 편리

finally

finally 블록은 어떤 경우라도 반드시 호출된다. 주로 try에서 사용한 자원을 해제할 때 사용한다.
예외를 직접 잡을 일이 없다면, try ~ finally만 사용하는 것도 가능하다.

정상 흐름 (try) -> finally
예외 잡음 (catch) -> finally
예외 던짐 -> finally (finally 블록 끝난 이후 예외가 밖으로 던져짐)

자원 해제

외부 리소스는 사용 후 반드시 연결을 해제하고 자원을 반납해야 메모리 고갈을 피할 수 있다. (네트워크 연결 자원, DB 연결 자원…)
자바는 GC로 JVM 메모리 상 인스턴스들을 자동으로 해제하지만, 외부 연결 같은 자바 외부 자원은 자동으로 해제되지 않는다.

여러 예외 한 번에 잡는 Syntax

catch 블록에서 | 키워드를 사용해 예외를 나열할 수 있다.
다만, 이 경우 각 예외들의 공통 부모 기능만 사용할 수 있다.
e.g. catch (ConnectException | SendException e) {...}
// 이 경우 공통 부모인 NetworkClientException의 기능만 사용 가능

try-with-resources 구문

  • finally 없이 편리한 외부 자원 해제 지원
  • 사용 방법
    • 외부 자원 클래스에 AutoCloseable 인터페이스를 구현 (implements AutoCloseable)
      • close() 메서드를 오버라이드해 자원 반납 방법 정의
      • 메서드가 예외를 던지지 않으면 인터페이스의 throws Exception생략
    • try-with-resources 구문 사용
        try (Resource resource = new Resource()) { 
            // 리소스를 사용하는 코드
        }
      
      • try 괄호 안에 사용할 자원을 명시
        • e.g. try (NetworkClient client = new NetworkClient(address)) {...}
      • try 블록이 끝나면 자동으로 AutoCloseable.close() 호출해 자원 해제
      • try 블록만 단독으로도 사용 가능 (catch, finally 없이 사용 가능)
  • 장점
    • 리소스 누수 방지
      • 실수로 finally 블록 혹은 그 내부에 자원 해제 코드누락하는 문제 예방
    • 코드 간결성가독성 향상
    • 리소스 스코프 범위 한정
      • 리소스 객체 변수의 스코프를 try 블록으로 한정해 코드 유지보수성 향상
    • 조금 더 빠른 자원 해제
      • 기존에는 catch 이후에 자원을 반납 (try -> catch -> finally)
      • try with resourcestry 블록이 끝나면 즉시 close() 호출

Reference

김영한의 실전 자바 - 중급 1편