[스프링 DB 1편] - 자바 예외 이해(1)

Chooooo·2023년 1월 19일
0

스프링 DB 1편

목록 보기
8/11
post-thumbnail

이 글은 강의 : 김영한님의 - "[스프링 DB 1편 - 데이터 접근 핵심 원리]"을 듣고 정리한 내용입니다. 😁😁


예외 계층

스프링이 제공하는 예외 추상화를 이해하기 위해서는 먼저 자바 기본 예외에 대한 이해가 필요하다. 예외는 자바 언어의 기본 문법에 들어가기 때문에 대부분 아는 내용일 것이다. 예외의 기본 내용을 간단히 복습하고, 실무에 필요한 체크 예외와 언체크 예외의 차이와 활용 방안에 대해서도 알아보자.

예외 계층 그림

🎈 Object : 예외도 객체이다. 모든 객체의 최상위 부모는 Object이므로 예외의 최상위 부모도 Object이다.

🎈 Throwable : 최상위 예외이다. 하위에 Exception 과 Error 가 있다.

🎈 Error : 메모리 부족이나 심각한 시스템 오류와 같이 애플리케이션에서 복구 불가능한 시스템 예외이다.

  • 애플리케이션 개발자는 이 예외를 잡으려고 해서는 안된다.
    • 상위 예외를 catch로 잡으면(예를 들어 Throwable을 catch로 잡으면 Error, Exception
      이런 것들이 다 같이 잡힘) 그 하위 예외까지 함께 잡는다. 따라서 애플리케이션 로직에서는 Throwable 예외도 잡으면 안되는데, 앞서 애기한 Error 예외도 함께 잡을 수 있기 때문. 애플리케이션 로직은 이런 이유로 Exception부터 필요한 예외로 생각하고 잡으면 된다.
      (Exception 계층부터 예외를 잡는다)
  • 참고로 Error도 언체크 예외이다.

🎈 Exception : 체크 예외

  • 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다.
    (체크 예외는 컴파일러가 컴파일 전, 예외가 존재하는지 체크해준다.)
  • Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다.RuntimeException은 예외로 한다.

🎈 RuntimeException : 언체크 예외, 런타임 예외

  • 컴파일러가 체크하지 않는 언체크 예외이다. (컴파일러가 체크하지 않는 예외이다.)
  • RuntimeException과 그 자식 예외는 모두 언체크 예외이다.
  • RuntimeException의 이름을 따라서 RuntimeException과 그 하위 언체크 예외를 런타임 예외라고 많이 부른다.

즉, 자바에서는 Exception 예외를 통해 애플리케이션 내에서 예외를 처리한다. 그리고 이 예외는 체크 예외와 언체크 예외로 나뉘는데, 체크 예외는 컴파일러가 컴파일 전에 체크를 해준다. 언체크 예외는 컴파일러가 예외가 발생하든 말든 상관없이 내버려 둔다 !


예외 기본 규칙

예외는 폭탄돌리기와 같다. 잡아서 처리하거나, 처리할 수 없으면 밖으로 던져야 한다.

🎃 예외를 Catch해서 처리해준다.
🎃 예외를 처리할 수 없으면 throws를 이용해 상위로 던진다.

  • (예를 들어서 Exception 을 catch 로 잡으면 그 하위 예외들도 모두 잡을 수 있다. Exception 을 throws 로 던지면 그 하위 예외들도 모두 던질 수 있다.)

예외는 이 두 가지 중 반드시 하나로 처리를 해야한다.

  • 예외 처리
  1. Controller → Service → Repository 순서로 호출된다.

  2. Repository 계층에서 예외가 발생된다. Repository는 예외를 처리할 수 없어 호출한 Service로 예외를 던진다.

  3. Service는 던져진 예외를 잡아서(Catch) 처리해준다. 예외가 처리되었기 때문에 정상적인 흐름으로 어플리케이션이 동작한다.

  • 예외 던짐
  1. Controller → Service → Repository 순으로 호출된다.

  2. Repository에서 예외 발생한다. 처리할 수 없어 Service 계층으로 던진다.

  3. Service 계층도 예외를 처리할 수 없어 Controller 계층으로 던진다.

🧨 이처럼 예외를 처리하지 못하면 호출한 곳으로 예외를 계속 던지게 된다.

예외를 처리하지 못하고 계속 던지면 어떻게 될까?

🎈 자바 main() 쓰레드

  • 예외 로그를 출력하면서 시스템이 종료된다.

🎈 Web Application

  • 여러 사용자의 요청을 처리하기 때문에 하나의 잘못된 요청으로 시스템이 종료되면 안된다. WAS가 예외를 받아서 처리하는데, 주로 사용자에게 (개발자가 지정한)오류 페이지를 보여준다.

이제 체크 예외와 언체크 예외의 차이에 대해서 알아보자.

체크 예외 기본 이해

Exception + 하위 예외는 체크 예외다. 이 체크 예외는 컴파일러가 컴파일 하기 전, 오류가 있는지를 확인한다. 체크 예외는 컴파일 전에 표시가 되며, 컴파일 하기 전에 모든 예외를 Catch로 처리 / Throw로 던지거나를 처리해야한다. 그렇게 하지 않을 경우 컴파일 오류가 발생한다.

체크 예외 전체 코드

  1. 체크 예외를 처리하지 않으면 계속 상위층으로 throw된다.
  2. 체크 예외를 catch해서 처리하는 경우 예외를 throw하지 않아도 된다.

MyCheckedException : 체크 예외 작성

/**
 * Exception 상속 예외 → 체크 예외
 */
static class MyCheckedException extends Exception{
    public MyCheckedException(String message) {
        super(message);
    }
}

🎃 Exception을 상속받은 MyCheckedException을 만든다.
🎃 Exception을 상속받았기 때문에 이 예외는 체크 예외가 된다. 따라서 컴파일 시점에 예외 처리를 완료해야한다.

Repository 클래스 생성

static class Repository{
    public void call() throws MyCheckedException {
        throw new MyCheckedException("ex");
    }
}

🎃 Repository 클래스를 생성한다.
🎃 Repository 클래스는 call() 메서드가 호출되면 MyCheckedException을 던져준다.

체크 예외는 예외를 던질 때 무조건 선언을 해줘야 한다.throws MyCheckedException

Service 클래스 생성

static class Service{
    Repository repository = new Repository();

    /**
     * Checked 예외는 Catch / Throw 하나를 선택해야함.
     */

    public void callCatch() {
        try {
            repository.call();
        } catch (MyCheckedException e) {
            log.info("예외 처리, message = {}", e.getMessage());
        }
    }

    public void callThrow() throws MyCheckedException {
        repository.call();
    }
}

🎈 Service 클래스는 Repository.call()을 통해 MyCheckedException을 불러온다.
🎈 두가지 메서드를 만든다.

  • callCatch() : Repository에서 던져진 Exception을 Catch로 처리한다.
  • callThrow() : Repository에서 던져진 Exception을 throws로 던진다.

컴파일러가 체크해준다고 해서 catch한다고 함. (예외를 잡음)

테스트 코드

@Test
void checked_Catch() {
    Service service = new Service();
    service.callCatch();
}

@Test
void checked_Throw() {
    Service service = new Service();
    assertThatThrownBy(() -> service.callThrow()).isInstanceOf(MyCheckedException.class);
}

checked_Throw()에서 service.callThrow() 로직을 호출 시에 해당 예외가 터져야 한다는 뜻!

🎈 checked_Catch()

  • callCatch()를 호출한다. 이 메서드는 서비스 영역에서 Repository에서 발생한 체크 예외를 처리해준다. 따라서 테스트 코드에서는 예외 처리를 위한 추가 로직을 처리할 필요가 없다.
    🎈 checked_Throw()
  • callThrow()를 호출한다. 이 메서드는 Repository에서 발생한 에외를 Service영역에서도 던져준다. 따라서 테스트 코드까지 예외가 올라온다.
  • 체크 예외는 예외를 잡지 않고 밖으로 던지려면 throws 예외를 메서드에 필수로 선언해야 한다.

정리

🎃 Exception을 상속 받은 예외는 체크 예외가 된다.

🎃 RunTimeException을 상속 받은 예외는 언체크 예외가 된다.

🎃 예외를 Catch / Throw 할 때 상위 타입의 예외를 던지면 하위 타입도 함께 처리가 된다.

  • Exception을 Throw 하는 것은 좋지 않다. 내가 처리할 수 있는 로직까지도 모두 Throw하기 때문이다. 따라서 내가 던질 예외만 명시적으로 작성하는 것이 좋은 코드가 된다.
    (자세하게 내가 던질 예외만 던져주자!)

체크 예외의 장/단점

체크 예외는 컴파일러가 컴파일 시점에 예외가 발생하는 것을 확인한다. 개발자는 이 예외를 Throw / Catch로 처리를 해줘야 정상적으로 컴파일을 처리할 수 있다.

🎈 장점

  • 컴파일러가 개발자가 실수로 예외를 누락하지 않도록 돕는다. (개발자가 예외를 놓치지 않도록 하는 안전장치) → 가장 좋은 오류는 컴파일 오류 !

🎈 단점

  • 개발자가 모든 체크 예외를 반드시 처리해야 한다. 신경 써도 아무 의미가 없는 예외까지 모두 신경을 써야된다.
  • 의존관계에 따른 단점도 존재한다.(Repository에서 발생한 SQL Exception이 Service 계층으로 올라온다) 의존관계에 따른 단점은 뒤에서 자세히 공부하자.

언체크 예외(Unchecked Exception)

🎃 언체크 예외는 기본적으로 체크 예외와 동일하지만, 컴파일러가 체크하지 않는다는 점이 다르다.

🎃 언체크 예외는 컴파일러가 체크하지 않기 때문에 Catch / Throws가 강제되지 않는다.

🎃 언체크 예외가 발생해서 Catch되지 않을 경우 자동으로 Throws 된다.

정리하면 체크 예외는 컴파일 시점에 반드시 예외를 catch/throws를 해야한다. 그렇지만 언체크 예외는 컴파일 시점에 처리되지 않은 예외가 발생할 경우, 자동으로 throws된다는 점이다.

MyUncheckedException

/**
 * RuntimeException 상속 예외 → 언체크 예외
 */
static class MyUnCheckedException extends RuntimeException{
    public MyUnCheckedException(String message) {
        super(message);
    }
}
  • RuntimeException을 상속받은 예외를 만든다.
  • RuntimeException을 상속받았기 때문에 이 예외는 언체크 예외가 된다.

Repository 클래스

static class Repository{
    public void call() throws MyUnCheckedException {
        throw new MyUnCheckedException("ex");
    }
}
  • Repository 클래스는 언체크 예외를 생성해서 던져준다.
    여기서 보면 메서드 라인에 예외를 던지지 않아도 된다 ! 물론 선언을 해도 되긴 함.throws MyUnCheckedException (생략 가능하다는 것이 중요)

  • (모든 예외는 잡거나 던지거나 둘 중 하나. 근데 그게 컴파일러가 체크를 하냐 안하냐의 차이일 뿐)

Service 클래스

static class Service{
    Repository repository = new Repository();

    public void callCatch() {
        try {
            repository.call();
        } catch (MyUnCheckedException e) {
            log.info("예외 처리, message = {}", e.getMessage());
        }
    }

    public void callThrow() {
        repository.call();
    }
}
  • catch를 하는 부분은 체크 예외와 동일하다. 단 강제 되지는 않는다. 잡고 싶지 않으면 잡지 않아도 된다.
  • throw를 하는 부분은 반드시 필수적이지는 않다. 언체크 예외는 명시적으로 예외를 잡거나 던지지 않으면 자동으로 throws를 해준다.

테스트 코드

@Test
void unChecked_Catch() {
    Service service = new Service();
    service.callCatch();
}

@Test
void unChecked_Throw() {
    Service service = new Service();
    assertThatThrownBy(() -> service.callThrow()).isInstanceOf(MyUnCheckedException.class);
}
  • callCatch()는 Service에서 예외가 처리된다.
  • callThrow()에서는 Service에서 명시적으로 예외를 던지지 않았는데, 자동으로 던져진다. 즉, 언체크 예외는 개발자가 직접 모든 예외를 신경쓰지 않아도 된다는 것을 보여준다.

언체크 예외의 장/단점

언체크 예외는 주로 생략을 한다. 그렇지만 중요한 예외의 경우 throws에 개발자가 명시해둘 수 있다. 이렇게 하면 특정 언체크 예외를 개발자가 알아보고 좀 더 신경을 써서 처리할 수 있다는 장점이 있다.

🎈 장점

  • 신경쓰고 싶지 않은 언체크 예외는 모두 무시할 수 있다.
  • 언체크 예외는 기본적으로 현재 위치에서 catch/throw가 되지 않는 경우 자동으로 상위 계층으로 throw되기 때문이다.

🎈 단점

  • 언체크 예외는 개발자가 실수로 예외를 누락할 수 있다. 즉 런타임 시점에 에러가 발생할 수 있다.
profile
back-end, 지속 성장 가능한 개발자를 향하여

0개의 댓글