본 포스팅은 [인프런] 스프링 DB 1편 - 데이터 접근 핵심 원리 강의를 바탕으로 공부하고 정리한 글입니다.
자바의 예외 계층
Object | • 예외도 객체이므로 최상위 부모가 Object이다. (자바의 모든 객체의 최상위 부모는 Object) |
Throwable | • 최상위 예외 |
Error | • 메모리 부족이나 시스템 오류와 같이 애플리케이션에서 복구 불가능한 시스템 예외 • 개발자는 이 예외를 잡으려고 해서는 안된다. |
Exception | • 체크 예외 • 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다. • 컴파일러가 체크하는 예외 (단 RuntimeException은 예외) |
RuntimeException | • 언체크 예외, 런타임 예외 • 컴파일러가 체크 하지 않는 예외 |
예외 처리 규칙
예외는 2가지 기본 규칙을 가진다.
- 예외는 잡아서 처리하거나 던져야 한다.
- 예외를 잡거나 던질때 지정한 예외뿐만 아니라 그 예외의 자식들도 함께 처리된다.
- ex) Exception을 catch로 잡으면 그 하위 예외들도 모두 잡을 수 있다.
- ex) Exception을 throws로 던지면 그 하위 예외들도 모두 던질 수 있다.
만약 예외를 처리하지 못하고 계속 던지면 어떻게 될까?
- 자바 main() 쓰레드의 경우 예외 로그를 출력하며 시스템 종료
- 웹 애플리케이션의 경우 여러 사용자의 요청을 처리하기 때문에 하나의 예외 때문에 시스템이 종료되어서는 안된다.
따라서 WAS가 해당 예외를 받아 처리하는데, 주로 사용자에게 개발자가 지정해준 오류 페이지를 보여준다.
체크 예외 vs 언체크 예외
체크 예외
- Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다. (RuntimeException은 예외)
- 체크 예외는 반드시 잡아서 처리하거나 밖으로 던지도록 throws 선언을 해야 한다.
- 그렇지 않을 경우 컴파일 오류가 발생한다.
- 비즈니스 의미가 있는 예외로 사용한다.
- 장점 : 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 안전 장치이다.
- 단점 : 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해줘야 하기 때문에, 신경쓰고 싶지 않은 예외까지 모두 챙겨야하므로 번거롭다.
@Slf4j
class CheckedTest {
@Test
void checked_catch() {
Service service = new Service();
service.callCatch();
}
@Test
void checked_throws() {
Service service = new Service();
assertThatThrownBy(() -> service.callThrow())
.isInstanceOf(MyCheckedException.class);
}
static class MyCheckedException extends Exception {
/**
* Exception을 상속받은 예외는 체크 예외가 된다.
*/
MyCheckedException(String message) {
super(message);
}
}
/**
* Checked 예외는
* 예외를 잡아서 처리하거나, 던지거나 둘중 하나를 필수로 선택해야 한다.
*/
static class Service {
Repository repository = new Repository();
/**
* 체크 예외를 잡아서 처리하는 코드
*/
void callCatch() {
try {
repository.call();
} catch (MyCheckedException e) {
// 예외 처리 로직
log.info("예외 처리, message={}", e.getMessage(), e);
}
}
/**
* 체크 예외를 밖으로 던지는 코드
* 체크 예외는 예외를 잡지 않고 밖으로 던지면 throws 예외를 메서드에 필수로 선언해야한다.
* @throws MyCheckedException
*/
void callThrow() throws MyCheckedException {
repository.call();
}
}
static class Repository {
void call() throws MyCheckedException {
throw new MyCheckedException("ex");
}
}
}
언체크 예외 (런타임 예외)
- RuntimeException과 그 하위 예외는 언체크 예외로 분류된다.
- 언체크 예외는 컴파일러가 예외를 체크하지 않는다.
- 체크 예외와 기본적으로 동일하지만, 언체크 예외는 예외를 던지는 throws를 선언하지 않고 생략할 수 있다. → 자동으로 예외를 던짐
- 복구 불가능한 예외로 사용한다.
- 장점 : 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다.
- 단점 : 개발자가 실수로 예외를 누락할 수 있다.
@Slf4j
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);
}
/**
* RuntimeException을 상속받은 예외는 언체크 예외가 된다.
*/
static class MyUncheckedException extends RuntimeException {
public MyUncheckedException(String message) {
super(message);
}
}
/**
* Unchecked 예외는
* 예외를 잡거나, 던지지 않아도 된다.
* 예외를 잡지 않으면 자동으로 밖으로 던진다.
*/
static class Service {
Repository repository = new Repository();
/**
* 필요한 경우 예와를 잡아서 처리하면 된다.
*/
void callCatch() {
try {
repository.call();
} catch (MyUncheckedException e) {
// 예외 처리 로직
log.info("예외 처리, message={}", e.getMessage(), e);
}
}
/**
* 예외를 잡지 않아도 된다. 자연스럽게 상위로 넘어간다.
* 체크 예와와 다르기 throws 예외 선언을 하지 않아도 된다.
*/
void callThrow() {
repository.call();
}
}
static class Repository {
void call() {
throw new MyUncheckedException("ex");
}
}
}
언제 체크 예외를 사용하고 언체크 예외를 사용할까?
- 기본적으로 언체크(런타임) 예외를 사용하자.
- 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용하자.
- 반드시 처리해야 하는 예외일 경우 컴파일러가 누락을 체크해주는 체크 예외를 사용해 놓치는 경우가 없도록 한다.
- 하지만 100% 체크 예외로 만들어야 하는 것은 아니다.
- 예) 계좌 이체 실패 예외, 결제시 포인트 부족 예외, 로그인 id 및 pw 불일치 예외
비즈니스적으로 의미가 있는 예외라는 것이 무슨 뜻일까?
간단한 예제로 생각해보자.
주문을 하는데 상황에 따라 다음과 같이 조치할 수 있다.
- 정상 : 주문시 결제를 성공하면 주문 데이터를 저장하고 결제 상태를 [완료]로 처리한다.
- 시스템 예외 (언체크) : 주문시 내부에 복구 불가능한 예외가 발생하면 전체 데이터를 롤백한다.
- 비즈니스 예외 (체크) : 주문시 결제 잔고가 부족하면 주문 데이터를 저장하고, 결제 상태를 [대기]로 처리한다. 이후에 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내한다.
이때 결제 잔고 부족 시 NotEnoughMoneyException이라는 체크 예외가 발생한다고 가정한다면, 이 예외는 시스템에 문제가 있어서 발생하는 예외가 아님을 생각해볼 수 있다. 고객의 잔고가 부족한 것은 시스템 상의 문제가 아니라 비즈니스 상황에서 문제가 되어 발생한 예외인 것이다.
이러한 예외를 비즈니스 예외라 한다. 비즈니스 예외는 매우 중요하고 반드시 처리해줘야 하는 경우가 많으므로 체크 예외를 고려할 수 있다.
단순히 생각해보면 체크 예외가 언체크 예외보다 더 안전하고 좋다고 생각이 든다.
그런데 왜 기본적으로 언체크 예외를 사용하는 것이 좋을까?
체크 예외는 다음과 같은 문제점이 있다.
- 복구 불가능한 예외
- 대부분의 예외는 복구가 불가능하며, 서비스나 컨트롤러는 이러한 문제를 해결할 수 없다. 따라서 이런 문제에 대해서는 오류 로그를 남기고 해당 오류를 개발자가 빨리 인지하는 것이 필요하다.
- 의존 관계에 대한 문제
- 대부분의 예외는 복구 불가능한 예외이지만 체크 예외이기 때문에 컨트롤러나 서비스 입장에서는 본인이 처리할 수 없어도 어쩔 수 없이 throws를 통해 던지는 예외를 선언해줘야 한다. 따라서 해당 예외 객체에 의존하게 된다.
- 이는 만약 JDBC 기술을 사용하다가 JPA로 변경했을 때 예를 들어 예외가 SQLException에서 JPAException으로 변경된다면 컨트롤러 및 서비스 클래스를 모두 변경해줘야 함을 의미한다.
따라서 잡고 싶은 예외만 잡을 수 있도록 언체크 예외를 기본적으로 사용한다.
예외를 처리하는 다양한 방법
스프링이 제공하는 추상화된 예외로 변환
스프링은 데이터 접근과 관련된 예외를 추상화해서 제공한다.
- DataAccessException 예외 (최상위)
- NonTransient 예외
- Transient 예외
이 예외들은 특정 기술(JDBC, JPA 등)에 종속적이지 않게 설계되어 있기 때문에 서비스 계층에서 예외 처리가 필요하다면 종속적인 SQLException을 직접 사용하는 것이 아니라 스프링이 제공하는 데이터 접근 예외를 사용하면 된다.
스프링은 예외 변환기(SQLExceptionTranslator)를 통해 적절한 스프링 데이터 접근 예외로 알아서 변환도 해준다.
Reference
체크 예외와 언체크 예외/런타임예외의 차이와 올바른 예외 처리 방법
[인프런] 스프링 DB 2편 - 데이터 접근 활용 기술 (예외와 트랜잭션 커밋, 롤백 활용)
'☕ Java > 이론' 카테고리의 다른 글
정적 팩토리 메서드(static factory method) (0) | 2023.06.12 |
---|---|
[Junit5] @ParameterizedTest 사용하기 (0) | 2022.10.11 |
[Java] 추상 클래스 (0) | 2022.08.30 |
[Java] 스트림 (Stream) (0) | 2022.02.08 |
[Java] Optional 클래스 (0) | 2022.02.07 |
[Java] 메소드 참조 (Method References) (0) | 2022.02.04 |
[Java] 함수형 인터페이스 (0) | 2022.02.03 |
[Java] 람다 (0) | 2022.01.28 |