[JAVA] 자바 예외처리 이해하기
📑 자바 예외 계층
🎈 Object : 예외도 객체이다. 모든 객체의 최상위 부모는 Object
🎈 Throwable : 최상위 예외이다. 하위에 Exception과 Error가 있다.
🎈 Error : 메모리 부족이나 심각한 시스템 오류와 같이 애플리케이션에서 복구 불가능한 시스템 예외이다. 애플리케이션 개발자는 이 예외를 잡으려고 해서는 안된다.
상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡는다. 따라서 애플리케이션 로직에서는 Throwable로 잡으면 안되는데, 앞서 이야기한 Error 예외도 잡을 수 있기 때문이다. 애플리케이션 로직은 이런 이유로 Exception 부터 필요한 예외로 생각하고 잡으면 된다.
🎈 Exception : 체크 예외
애플리케이션 로직에 사용할 수 있는 실질적인 최상위 예외이다.
Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다. 단 RuntimeException은 예외로 한다.
🎈 RuntimeException : 언체크 예외, 런타임 예외
컴파일러가 체크하지 않는 언체크 예외이다. RuntimeException 이름에 따라서 RuntimeException과 그 하위 언체크 예외를 런타임 에러라고 많이 부른다.
📑 예외 처리 기본 규칙
예외는 잡아서 처리하거나, 처리할 수 없으면 밖으로 던져야한다.
📑 체크 예외 예제
위의 코드를 살펴보면 예외를 던지고 있다. 이렇게 던질때는 extends로 밖으로 던지는 걸 선언해야한다. 아님 빨간줄 뜬다.
아래 코드는 예외를 잡아서 try~catch로 처리하는 것.
예외를 던지는 테스트를 진행할때 이렇게 아래 코드처럼 짜게 되면 예외가 발생했을 때 에러를 터뜨리기 때문에 아래 주석처리한 코드처럼 만약 에러가 발생하면 에러 대신에 MyCheckedException.class를 호출해줘라는 코드로 작성해야 한다.
package hello.jdbc.exception.basic;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
@Slf4j
public class CheckedTest {
@Test
void checked_catch(){
Service service = new Service();
service.callCatch();
}
@Test
void checked_throw(){
Service service = new Service();
Assertions.assertThatThrownBy(()->service.callThrow()).isInstanceOf(MyCheckedException.class);
}
/**
* Exception을 상속받은 예외는 체크 예외가 된다.
*/
static class MyCheckedException extends Exception{
//컴파일러가 체크하는 체크 예외
public MyCheckedException(String message) {
super(message);
}
}
/**
* checked 예외는
* 예외를 잡아서 처리하거나, 던지거나 둘중 하나를 필수로 선택해야한다.
*/
static class Service {
Repository repository = new Repository();
/**
* 예외를 잡아서 처리하는 코드
* */
public void callCatch() {
try {
repository.call();
} catch (MyCheckedException e) {
log.info("예외 처리, message={}", e.getMessage(),e);
}
}
/**
* 체크 예외를 밖으로 던지는 코드
* 체크 예외는 예외를 잡지 않고 밖으로 던지려면 throws 예외를 메서드에 필수로 선언해야한다.
* @throws MyCheckedException
*/
public void callThrow() throws MyCheckedException {
repository.call();
}
}
static class Repository{
public void call() throws MyCheckedException {
//exception을 터뜨리는다.
throw new MyCheckedException("ex"); //던지려면 무조건 throws로 선언을 해야된다.
}
}
}
📑 언체크 예외 예제
언체크 예외는 체크 예외와 기본적으로 동일하다. 차이가 있다면 예외를 던지는 throws를 선언하지 않고, 생략할 수 있다는 것! 이 경우 자동으로 예외를 던진다.
package hello.jdbc.exception.basic;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
@Slf4j
public class UncheckedTest {
@Test
void unchecked_catch(){
Service service = new Service();
service.callCatch();
}
@Test
void unchecked_throw(){
Service service = new Service();
Assertions.assertThatThrownBy(()->service.callThrow()).isInstanceOf(MyUncheckedException.class);
}
/**
* RuntimeException을 상속받은 예외는 언체크 예외가 된다.
*/
static class MyUncheckedException extends RuntimeException{
public MyUncheckedException(String message) {
super(message);
}
}
/**
* Uncehced 예외는 예외를 잡거나, 던지지 않아도 된다.
* 예외를 잡지 않으면 자동으로 밖으로 던진다.
*/
static class Service{
Repository repository = new Repository();
/**
* 필요한 경우 예외를 잡아서 처리하면 된다.
*/
public void callCatch(){
try{
repository.call();
}catch (MyUncheckedException e){
log.info("예외 처리, message={}", e.getMessage(),e);
}
}
/**
* 예외를 잡지 않아도 된다. 자연스럽게 상위로 넘어간다.
* 체크 예외와 다르게 throws 예외 선언을 하지 않아도 된다.
*/
public void callThrow(){
repository.call();
}
}
static class Repository{
public void call(){
throw new MyUncheckedException("ex");
}
}
}
📑 체크 VS 언체크 언제 사용할까?
🎈 기본적으로 언체크(런타임) 예외를 사용하자
🎈 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외만 사용하자. 이 경우 해당 예외를 잡아서 반드시 처리해야 하는 문제일 때만 체크 예외를 사용한다. (ex. 계좌 이체 실패 예외, 결제시 포인트 부족 예외, 로그인 ID, PW 불일치 예외)