이 글은 강의 : 김영한님의 - "[스프링 DB 1편 - 데이터 접근 핵심 원리]"을 듣고 정리한 내용입니다. 😁😁
스프링이 제공하는 예외 추상화를 이해하기 위해서는 먼저 자바 기본 예외에 대한 이해가 필요하다. 예외는 자바 언어의 기본 문법에 들어가기 때문에 대부분 아는 내용일 것이다. 예외의 기본 내용을 간단히 복습하고, 실무에 필요한 체크 예외와 언체크 예외의 차이와 활용 방안에 대해서도 알아보자.
🎈 Object
: 예외도 객체이다. 모든 객체의 최상위 부모는 Object
이므로 예외의 최상위 부모도 Object
이다.
🎈 Throwable
: 최상위 예외이다. 하위에 Exception 과 Error 가 있다.
🎈 Error
: 메모리 부족이나 심각한 시스템 오류와 같이 애플리케이션에서 복구 불가능한 시스템 예외이다.
Exception
부터 필요한 예외로 생각하고 잡으면 된다.🎈 Exception
: 체크 예외
Exception
과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다. 단 RuntimeException
은 예외로 한다.🎈 RuntimeException
: 언체크 예외, 런타임 예외
RuntimeException
과 그 자식 예외는 모두 언체크 예외이다.RuntimeException
의 이름을 따라서 RuntimeException
과 그 하위 언체크 예외를 런타임 예외라고 많이 부른다. 즉, 자바에서는 Exception 예외를 통해 애플리케이션 내에서 예외를 처리한다. 그리고 이 예외는 체크 예외와 언체크 예외로 나뉘는데, 체크 예외는 컴파일러가 컴파일 전에 체크를 해준다. 언체크 예외는 컴파일러가 예외가 발생하든 말든 상관없이 내버려 둔다 !
예외는 폭탄돌리기와 같다. 잡아서 처리하거나, 처리할 수 없으면 밖으로 던져야 한다.
🎃 예외를 Catch해서 처리해준다.
🎃 예외를 처리할 수 없으면 throws를 이용해 상위로 던진다.
예외는 이 두 가지 중 반드시 하나로 처리를 해야한다.
Controller → Service → Repository 순서로 호출된다.
Repository 계층에서 예외가 발생된다. Repository는 예외를 처리할 수 없어 호출한 Service로 예외를 던진다.
Service는 던져진 예외를 잡아서(Catch) 처리해준다. 예외가 처리되었기 때문에 정상적인 흐름으로 어플리케이션이 동작한다.
Controller → Service → Repository 순으로 호출된다.
Repository에서 예외 발생한다. 처리할 수 없어 Service 계층으로 던진다.
Service 계층도 예외를 처리할 수 없어 Controller 계층으로 던진다.
🧨 이처럼 예외를 처리하지 못하면 호출한 곳으로 예외를 계속 던지게 된다.
🎈 자바 main() 쓰레드
🎈 Web Application
이제 체크 예외와 언체크 예외의 차이에 대해서 알아보자.
Exception + 하위 예외는 체크 예외다. 이 체크 예외는 컴파일러가 컴파일 하기 전, 오류가 있는지를 확인한다. 체크 예외는 컴파일 전에 표시가 되며, 컴파일 하기 전에 모든 예외를 Catch로 처리 / Throw로 던지거나를 처리해야한다. 그렇게 하지 않을 경우 컴파일 오류가 발생한다.
/**
* Exception 상속 예외 → 체크 예외
*/
static class MyCheckedException extends Exception{
public MyCheckedException(String message) {
super(message);
}
}
🎃 Exception을 상속받은 MyCheckedException을 만든다.
🎃 Exception을 상속받았기 때문에 이 예외는 체크 예외가 된다. 따라서 컴파일 시점에 예외 처리를 완료해야한다.
static class Repository{
public void call() throws MyCheckedException {
throw new MyCheckedException("ex");
}
}
🎃 Repository 클래스를 생성한다.
🎃 Repository 클래스는 call() 메서드가 호출되면 MyCheckedException을 던져준다.
체크 예외는 예외를 던질 때 무조건 선언을 해줘야 한다.throws MyCheckedException
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을 불러온다.
🎈 두가지 메서드를 만든다.
컴파일러가 체크해준다고 해서 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()
🎃 Exception을 상속 받은 예외는 체크 예외가 된다.
🎃 RunTimeException을 상속 받은 예외는 언체크 예외가 된다.
🎃 예외를 Catch / Throw 할 때 상위 타입의 예외를 던지면 하위 타입도 함께 처리가 된다.
체크 예외는 컴파일러가 컴파일 시점에 예외가 발생하는 것을 확인한다. 개발자는 이 예외를 Throw / Catch로 처리를 해줘야 정상적으로 컴파일을 처리할 수 있다.
🎈 장점
🎈 단점
🎃 언체크 예외는 기본적으로 체크 예외와 동일하지만, 컴파일러가 체크하지 않는다는 점이 다르다.
🎃 언체크 예외는 컴파일러가 체크하지 않기 때문에 Catch / Throws가 강제되지 않는다.
🎃 언체크 예외가 발생해서 Catch되지 않을 경우 자동으로 Throws 된다.
정리하면 체크 예외는 컴파일 시점에 반드시 예외를 catch/throws를 해야한다. 그렇지만 언체크 예외는 컴파일 시점에 처리되지 않은 예외가 발생할 경우, 자동으로 throws된다는 점이다.
/**
* RuntimeException 상속 예외 → 언체크 예외
*/
static class MyUnCheckedException extends RuntimeException{
public MyUnCheckedException(String message) {
super(message);
}
}
static class Repository{
public void call() throws MyUnCheckedException {
throw new MyUnCheckedException("ex");
}
}
Repository 클래스는 언체크 예외를 생성해서 던져준다.
여기서 보면 메서드 라인에 예외를 던지지 않아도 된다 ! 물론 선언을 해도 되긴 함.throws MyUnCheckedException
(생략 가능하다는 것이 중요)
(모든 예외는 잡거나 던지거나 둘 중 하나. 근데 그게 컴파일러가 체크를 하냐 안하냐의 차이일 뿐)
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();
}
}
@Test
void unChecked_Catch() {
Service service = new Service();
service.callCatch();
}
@Test
void unChecked_Throw() {
Service service = new Service();
assertThatThrownBy(() -> service.callThrow()).isInstanceOf(MyUnCheckedException.class);
}
언체크 예외는 주로 생략을 한다. 그렇지만 중요한 예외의 경우 throws에 개발자가 명시해둘 수 있다. 이렇게 하면 특정 언체크 예외를 개발자가 알아보고 좀 더 신경을 써서 처리할 수 있다는 장점이 있다.
🎈 장점
🎈 단점