프레임워크(Framework)/Spring

[Spring DB] 체크 예외와 언체크 예외 총정리 - (1)

잇트루 2022. 11. 21. 23:44
반응형
본 내용은 온라인 강의 사이트 인프런의 김영한 님의 강의 내용이 포함되어 있습니다.
'스프링 DB 1편 - 데이터 접근 핵심 원리'
 

스프링 DB 1편 - 데이터 접근 핵심 원리 - 인프런 | 강의

백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., - 강의

www.inflearn.com

 

예외 계층

체크 예외와 언체크 예외를 알기 위해서는 자바의 예외에 대해 알 필요가 있다.

자바에서의 예외는 다음과 같은 형태의 계층으로 이루어져 있다.

  • Object : 예외도 객체이기 때문에 모든 객체의 최상위 부모인 Object를 상속받는다. 따라서 예외의 최상위 부모도 Object이다.
  • Throwable : 최상위 예외로 하위에는 Exception(예외)과 Error(에러)가 있다.

 

Error(에러)

Error는 메모리 부족이나 심각한 시스템 오류와 같은 애플리케이션에서 복구 불가능한 시스템 예외이다. 따라서 애플리케이션 개발자는 Error를 try~catch 문으로 잡으려고 해서는 안 된다.

  • 상위 예외를 catch로 잡으면 해당 하위 예외까지 함께 처리된다. 따라서 애플리케이션 로직에서는 Throwable 예외를 잡으면 안 된다. 이는 Error도 포함하여 함께 예외로 잡을 수 있기 때문이다.
  • Error는 언체크 예외이다.

 

Exception(체크 예외)

  • 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다.
  • Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다.
  • 단, RuntimeException은 Exception의 자식이지만 제외한다.

 

RuntimeException(언체크 예외)

  • 컴파일러가 체크하지 않는 언체크 예외이다.
  • RuntimeException과 그 자식 예외들은 모두 언체크 예외로 런타임 예외 또는 런타임 에러라고 부른다.

 

예외 기본 규칙

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

 

예외 처리

  • Repository에서 예외가 발생했을 경우, Repository에 접근하는 로직에서 예외를 처리하게 된다.
  • 이후 처리된 예외는 정상 흐름으로 동작한다.

 

예외 던짐

  • 예외가 발생하지 못할 경우, 해당 로직을 호출한 곳으로 예외를 계속 던지게 된다.

 

예외의 2가지 기본 규칙

  1. 예외는 잡아서 처리하거나 밖으로 던져야 한다.
  2. 예외를 잡거나 던질 때 지정한 예외뿐만 아니라 그 예외의 자식들도 함께 처리된다.
    • Exception을 catch로 잡으면, 해당 하위 예외(SQLException, IOException, RuntimeException)들도 모두 잡을 수 있다.
    • Exception을 throws로 던지면, 해당 하위 예외(SQLException, IOException, RuntimeException)들도 모두 던질 수 있다.

 

참고

만약, 예외를 처리하지 않고 계속 던지면 자바의 main() 메서드에서는 해당 예외 로그를 출력하면서 시스템이 종료된다.

하지만, 웹 애플리케이션의 경우에는 시스템이 종료하면 안 되기 때문에 WAS가 해당 예외를 처리하게 된다. 주로 개발자가 지정한 오류 페이지를 사용자에게 보여주게 된다.

 

체크 예외(Checked Exception)

체크 예외는 RuntimeExcetpion을 제외한 Exception와 모든 하위 예외는 컴파일러가 체크하는 체크 예외이다.

체크 예외는 잡아서(catch) 처리하거나, 또는 밖으로 던져서(throws) 해결해야 한다.

 

체크 예외 예제

MyCheckedExcpetion

class MyCheckedException extends Exception {
    public MyCheckedException(String message) {
        super(message);
    }
}

Exception을 상속받는 MyCheckedException은 체크 예외 클래스가 된다.

 

Repository

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

Repository 클래스 내부에 위에서 만든 MyCheckedException 예외를 발생시키는 메서드를 작성한다.

 

Service

@Slf4j
class Service {
    Repository repository = new Repository();

    // 예외를 잡아서 처리하는 코드
    public void callCatch() {
        try {
            repository.call();
        } catch (MyCheckedException e) {
            // 예외 처리 로직
            log.info("예외 처리, message={}", e.getMessage(), e);
        }
    }

    // 예외를 밖으로 던지는 코드
    public void callThrow() throws MyCheckedException {
        repository.call();
    }
}
  • Service 클래스에서 Repsitory의 call 메서드를 호출하여 예외를 발생시킨다.
  • Service에서는 예외를 잡아서(catch) 처리하거나, 밖으로 던져야(throws) 한다.
  • callCatch() 메서드는 예외를 잡아서 처리하는 메서드이고, callThrow() 메서드는 예외를 밖으로 던지는 메서드이다.

 

예외를 잡아서 처리하는 경우

try ~ catch문을 사용하여 try에서 호출을 하고, catch에서 예외 발생 시 처리 로직을 작성한다.

 

예외를 밖으로 던지는 경우

throws를 메서드에 필수로 선언하여 밖으로 던질 수 있다.

 

CheckedTest

public class CheckedTest {
    @Test
    void checked_catch() {
        Service service = new Service();
        service.callCatch();
    }

    @Test
    void checked_throw() {
        Service service = new Service();

        // service.callThrow()를 호출하면, MyCheckedException 예외가 발생해야 검증 성공
        assertThatThrownBy(() -> service.callThrow())
                .isInstanceOf(MyCheckedException.class);
    }
}

위에서 작성한 MyCheckedException과 Repository, Service를 이용하여 예외에 대한 테스트를 진행한다.

 

checked_catch() 실행 결과

실행 순서

  1. 테스트 실행 → service.callCatch() 호출 → repository.call() 호출 (예외 발생)
  2. repository.call() (예외 던짐) → service.callCatch() (예외 처리) → 테스트 종료(정상 종료)
log.info("예외 처리, message={}", e.getMessage(), e);

 

서비스 로직에서 예외 처리 코드 블록 내에 로그를 남겼기 때문에 아래와 같은 예외 결과를 출력하게 된다.

02:21:44.182 [main] INFO hello.jdbc.exception.basic.CheckedTest - 예외 처리, message=ex
hello.jdbc.exception.basic.CheckedTest$MyCheckedException: ex
	at hello.jdbc.exception.basic.CheckedTest$Repository.call(CheckedTest.java:71)
	at hello.jdbc.exception.basic.CheckedTest$Service.callCatch(CheckedTest.java:49)
	at hello.jdbc.exception.basic.CheckedTest.checked_catch(CheckedTest.java:16)
...

 

checked_throw() 실행 결과

실행 순서

  1. 테스트 실행 → service.callThrow() 호출 → repository.call() 호출 (예외 발생)
  2. repository.call() (예외 던짐) → service.callCatch() (예외 던짐) → 테스트 종료(예외 도착)

 

정상적으로 테스트가 통과되면 아무것도 출력이 되지 않고 실행 종료된다.

// service.callThrow()를 호출하면, MyCheckedException 예외가 발생해야 검증 성공
assertThatThrownBy(() -> service.callThrow()).isInstanceOf(MyCheckedException.class);

테스트 코드 작성 시 자주 쓰이는 검증 라이브러리 Assertions를 사용한 코드로 assertThatThrownBy는 실행 코드가 예외를 발생하면 통과하게 된다.

 

체크 예외 참고 사항

체크 예외를 잡아서 처리하려면 catch 문을 통해 예외를 잡을 수 있다.

try {
    repository.call();
} catch (MyCheckedException e) {
    // 예외 처리 로직
    log.info("예외 처리, message={}", e.getMessage(), e);
}
  • MyCheckedException 예외를 잡아서 처리한다.

 

catch는 해당 타입과 그 하위 타입을 모두 잡을 수 있다.

try {
    repository.call();
} catch (Exception e) {
    // 예외 처리 로직
    log.info("예외 처리, message={}", e.getMessage(), e);
}
  • catch에 Exception으로 작성해도 MyCheckedException 예외를 잡을 수 있다.
  • 하지만, 이 경우에는 런타임 에러까지 잡기 때문에 정확성을 높이기 위해서는 해당 타입을 적어야 한다.

 

체크 예외를 밖으로 던지는 경우 throws를 지정하지 않으면 컴파일 오류가 발생한다.

public void callThrow() {
	repository.call();
}
  • throws를 지정하지 않았기 때문에 컴파일 오류가 발생한다.
  • 체크 예외를 던지는 경우 throws를 필수적으로 지정해 주어야 한다.

 

또한 체크 예외를 밖으로 던지는 경우에도 해당 타입과 그 하위 타입 모두 던질 수 있다.

public void callThrow() throws Exception {
	repository.call();
}
  • throws에 Exception을 작성할 경우 MyCheckedException 예외까지 던질 수 있다.
  • catch와 마찬가지로 런타임 에러까지 던지기 때문에 정확성을 높이기 위해서는 해당 타입을 적어야 한다.

 

체크 예외의 장단점

체크 예외는 잡아서 처리할 수 없을 경우, 예외를 밖으로 던지는 throws를 필수적으로 선언해야 한다.

그렇지 않으면 컴파일 오류가 발생하며, 이로 인해 장단점이 동시에 존재한다.

 

장점

  • 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아준다.

 

단점

  • 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에 번거롭다.
  • 불필요한 의존관계 문제가 발생한다.

 

언체크 예외(Unchecked Exception)

언체크 예외는 컴파일러가 예외를 체크하지 않는다는 뜻으로, RuntimeException과 그 하위 예외를 말한다.

언체크 예외는 체크 예외와 동일하지만, 예외를 던지는 throws를 생략할 수 있다.

 

언체크 예외 예제

MyUncheckedException

class MyUncheckedException extends RuntimeException {
    public MyUncheckedException(String message) {
        super(message);
    }
}

RuntimeException을 상속받은 클래스는 언체크 예외 클래스가 된다.

 

Repository

class Repository {
    public void call() {
        throw new MyUncheckedException("ex");
    }
}

Repository 클래스 내부에는 위에서 만든 MyUncheckedException 예외를 발생시키는 메서드를 작성한다.

 

Service

@Slf4j
class Service {
    Repository repository = new Repository();

    // 예외를 잡아서 처리하는 코드
    public void callCatch() {
        try {
            repository.call();
        } catch (MyUncheckedException e) {
            log.info("예외 처리, message={}", e.getMessage(), e);
        }
    }

    // 예외를 밖으로 던지는 코드
    public void callThrow() {
        repository.call();
    }
}
  • Service 클래스에서 Repsitory의 call 메서드를 호출하는 로직을 작성하여 예외를 발생시킨다.
  • callCatch() 메서드는 예외를 잡아서 처리하는 메서드이고, callThrow() 메서드는 예외를 밖으로 던지는 메서드이다.
  • 체크 예외와의 차이점은 callThrow() 메서드에서 throws를 선언하지 않아도 되는 것이다. (자동으로 상위로 예외를 던짐)
  • 즉, 필요한 경우에는 예외를 잡아서 처리하며, 그렇지 않은 경우에는 자동으로 예외를 밖으로 던지게 된다.

 

UncheckedTest

public class UncheckedTest {

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

    @Test
    void unchecked_throw() {
        Service service = new Service();

        assertThatThrownBy(() -> service.callThrow())
                .isInstanceOf(MyUncheckedException.class);
    }
}

위에서 작성한 MyUncheckedException과 Repository, Service를 이용하여 예외에 대한 테스트를 진행한다.

 

unchecked_catch() 실행 결과

실행 순서

체크 예외와 실행 순서와 동일하다.

  1. 테스트 실행 → service.callCatch() 호출 → repository.call() 호출 (예외 발생)
  2. repository.call() (예외 던짐) → service.callCatch() (예외 처리) → 테스트 종료(정상 종료)
log.info("예외 처리, message={}", e.getMessage(), e);

 

서비스 로직에서 예외 처리 코드 블록 내에 로그를 남겼기 때문에 아래와 같은 예외 결과를 출력하게 된다.

00:00:44.694 [main] INFO hello.jdbc.exception.basic.UncheckedTest - 예외 처리, message=ex
hello.jdbc.exception.basic.UncheckedTest$MyUncheckedException: ex
	at hello.jdbc.exception.basic.UncheckedTest$Repository.call(UncheckedTest.java:63)
	at hello.jdbc.exception.basic.UncheckedTest$Service.callCatch(UncheckedTest.java:46)
...

 

언체크 예외 참고사항

public void callThrow() throws MyCheckedException {
    repository.call();
}
  • 언체크 예외도 throws를 선언해도 된다.
  • 중요한 예외인 경우 선언하여 코드를 통해 편리하게 인지할 수 있다.

 

언체크 예외의 장단점

언체크 예외는 예외를 잡아서 처리할 수 없을 때, 예외를 밖으로 던지는 throws를 생략할 수 있다. 언체크 예외 또한 이러한 이유로 장단점이 존재한다.

 

장점

  • 신경 쓰고 싶지 않은 언체크 예외를 무시할 수 있다.
  • 체크 예외는 처리할 수 없는 경우에 항상 throws를 통해 밖으로 던져야 하지만, 체크 예외는 이 부분을 생략할 수 있다.
  • 불필요한 의존관계를 굳이 참조하지 않아도 된다.

 

단점

  • 개발자의 실수로 예외를 누락할 수 있다.
반응형