자바 예외
예외 계층
- 예외 계층 구조
- Object : 모든 객체의 최상위 부모이기에 예외의 최상위 부모이다.
- Throwable : 최상위 예외로 하위에 Exception, Error가 있다.
- Error : 메모리 부족, 시스템 오류와 같은 애플리케이션에서 복구 불가능한 시스템 예외이다.
- 애플리케이션 개발자는 Error 예외를 잡아서는 안된다.
상위 예외를 잡으면 하위 예외도 잡히므로 Throwable도 잡아서는 안된다.
- 애플리케이션 개발자는 Error 예외를 잡아서는 안된다.
- Exception : 체크 예외로 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다.
- RuntimeException을 제외한 Exception의 하위 예외(SQLException, IOException ...)는 모두 체크 예외이다.
- RuntimeException : 언체크 예외, 런타임 예외이다.
- RuntimeException의 자식도 모두 언체크 예외이다.
- 체크 예외 : 컴파일러가 체크하는 예외이다.
- 언체크 예외 : 컴파일러가 체크하지 않는 예외이다.
예외 기본 규칙
- 예외는 폭탄 돌리기와 같다.
잡아서 처리하거나 처리할 수 없다면 밖으로 던져야한다.
- 예외를 처리하는 경우(catch) => 애플리케이션 로직이 정상 흐름으로 동작한다.
- 예외를 처리하지 못하는 경우(throws) => 호출한 곳으로 예외를 계속 던지게 된다.
- 예외 기본 규칙
- 예외는 잡아서 처리하거나 던져야한다.
- 예외를 잡거나 던질 때 지정한 예외 뿐만 아니라 그 자식들도 함께 처리된다.
- 예외를 처리하지 못하고 계속 던지는 경우
- main() 쓰레드의 경우 : 예외 로그를 출력하면서 시스템이 종료된다.
- 웹 애플리케이션의 경우 : 시스템이 종료되면 안되기에 WAS가 예외를 처리하는데 주로 사용자에게 개발자가 지정한 오류페이지를 보여준다.
- throw new 예외 : 새로운 예외를 발생시킬 수 있다.
- throw : 예외를 발생시키는 것.
- 예외도 객체이기 때문에 객체를 먼저 new로 생성하고 throw를 통해 예외를 발생시켜야 한다.
체크 예외
- RuntimeException을 제외한 Exception을 포함한 하위 예외들은 모두 체크 예외이다.
- 체크 예외는 잡아서 처리하거나 밖으로 던지도록 선언해야하며 그렇지 않으면 컴파일 오류가 발생한다.
- Exception 클래스를 상속받으면 체크 예외가 된다.
- RuntimeException 클래스를 상속받으면 언체크 예외가 된다.
public class MyCheckedException extends Exception {
public MyCheckedException(String message) {
super(message);
}
}
- 예외가 제공하는 기본 기능 중 오류 메시지를 보관하는 기능이 있다.
- 생성자를 통해서 해당 기능을 사용하면 편리하다.
package hello.jdbc.exception.basic;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
@Slf4j
public class CheckTest {
@Test
public void checkedCatch(){
Service service = new Service();
service.callCatch();
}
@Test
public void checkedThrow() {
Service service = new Service();
assertThatThrownBy(() -> service.callThrow())
.isInstanceOf(MyCheckedException.class);
}
static class MyCheckedException extends Exception {
public MyCheckedException(String message) {
super(message);
}
}
static 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();
}
}
static class Repository {
public void call() throws MyCheckedException {
throw new MyCheckedException("ex");
}
}
}
- 테스트 코드를 통한 체크 예외 처리 예제
- checkedCatch() 흐름 (체크 예외 잡아서 처리)
- service.callCatch() 호출
- repository.call() 호출 => 예외 발생 => 자신을 호출한 service.callCatch()로 예외 던짐
- service.callCatch()에서 catch로 예외 처리, 이후 정상 흐름
- checkedThrow() 흐름 (체크 예외 잡아서 처리하지 않고 밖으로 던지기)
- service.callThrow() 호출
- repository.call() 호출 => 예외발생 => 자신을 호출한 service.callThrow()로 예외 던짐
- service.callThrow()에서 자신을 호출한 checkThrow()로 예외 던짐
- checkedThrow()에서 Assertions.assertThatThrownBy()로 던져진 예외와 예상한 예외가 일치하는지 확인
- 로깅 시 마지막 파라미터로 예외 객체를 넣어주면 로그가 해당 예외의 스택 트레이스를 추가로 출력해준다.
- ex) log.info("예외 처리, message={}", e.getMessage(), e);
- checkedCatch() 흐름 (체크 예외 잡아서 처리)
- catch에 상위 타입 Exception을 적어줘도 자식 타입의 예외를 모두 잡을 수 있다.
- 보통 상위 타입 예외를 잡기보다는 구체적인 예외를 잡는 것이 더 좋다.
- 체크 예외를 처리할 수 없는 경우 method() throws 예외를 사용해서 밖으로 던질 예외를 필수로 지정해줘야 한다.
- throws를 지정하지 않으면 컴파일 오류가 발생한다.
- throws로 상위 타입을 밖으로 던져도 자식 타입도 던질 수 있지만 throws Exception은 모든 예외를 던지는 안좋은 코드로 구체적인 예외를 지정해서 던지는 것을 권장한다.
- 체크 예외의 장단점
- 예외를 잡아서 처리할 수 없을 때 예외를 밖으로 던지는 throws 예외를 필수로 선언해야 하며 누락되는 경우 오류가 발생한다.
- 장점 : 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 안전장치이다.
- 단점 : 모든 체크 예외를 반드시 잡거나 던지도록 처리해야하기 때문에 번거롭다.
언체크 예외
- 언체크 예외 : RuntimeException과 그 하위 예외로 컴파일러가 체크하지 않는 예외를 뜻한다.
- 언체크 예외도 잡아서 던지거나 처리해야하지만 예외를 던지는 throws를 생략할 수 있으며 이 경우 자동으로 예외를 던진다.
- RuntimeException을 상속받는 예외는 모두 언체크 예외가 된다.
- 체크 예외 vs 언체크 예외
- 체크 예외 : 예외를 잡아서 처리하지 않는 경우 항상 throws로 던지는 예외를 선언해야 한다.
- 언체크 예외 : 예외를 잡아서 처리하지 않아도 throws를 생략할 수 있고 자동으로 던져준다.
- 차이점은 예외를 처리할 수 없을 때 밖으로 던지는 부분을 필수로 선언해야하는지 생략할 수 있는지의 차이이다.
@Slf4j
public class UncheckedTest {
@Test
public void uncheckedCatch() throws Exception {
Service service = new Service();
service.callCatch();
}
@Test
public void uncheckedThrow() throws Exception {
Service service = new Service();
Assertions.assertThatThrownBy(() -> service.callThrow())
.isInstanceOf(MyUncheckedException.class);
}
static class MyUncheckedException extends RuntimeException {
public MyUncheckedException(String message) {
super(message);
}
}
static 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();
}
}
static class Repository {
public void call() {
throw new MyUncheckedException("ex");
}
}
}
- 언체크 예외 테스트 코드 예제
- 체크 예외와 흐름은 동일하다.
- 체크 예외와 다른 점은 언체크 예외에서는 throws로 예외를 선언해주지 않아도 자동으로 던져준다는 점이다.
- 언체크 예외도 throws 예외를 선언할 수 있지만 주로 생략한다.
- 중요한 예외의 경우 선언해두면 IDE를 통해 개발자가 좀 더 편리하게 인지할 수 있다.
- 언체크 예외의 장단점
- 예외를 잡아서 처리할 수 없을 때 예외를 밖으로 던지는 throws 예외 선언을 생략할 수 있다.
- 장점 : 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다.
- 단점 : 언체크 예외는 개발자가 실수로 예외를 누락할 수 있다.
체크 예외 활용
- 기본 원칙 2가지
- 기본적으로 언체크(런타임) 예외를 사용하자
- 체크 예외는 비지니스 로직상 의도적으로 던지는 예외에만 사용하자
- 해당 예외를 잡아서 반드시 처리해야하는 문제일 때만 체크 예외 사용
- ex) 계좌 이체 실패 예외, 로그인 ID, PW 불일치 예외
- 매우 심각한 문제는 개발자가 실수로 예외를 놓치면 안되기에 체크 예외로 만들어두면 컴파일러가 놓친 예외를 체크함으로 인지할 수 있다.
- 체크 예외의 문제점
- 체크 예외는 컴파일러가 예외 누락을 체크해주기 때문에 개발자가 실수로 예외를 놓치는 것을 방지해주지만 처리할 수 없을 때는 항상 예외를 던지도록 throws 예외를 선언해줘야 한다.
- 그림 예시의 경우와 같이 체크 예외가 발생하면 서블릿 오류 페이지나 ControllerAdvice에서 공통으로 처리하기에 Service와 Controller에서는 처리를 할 수 없기에 알 필요가 없는 예외를 밖으로 던져야한다.
- 문제점
- 복구 불가능한 예외
- 대부분의 실무에서 발생하는 예외는 DB나 네트워크 통신처럼 시스템 예외로 복구가 불가능하며 서비스나 컨트롤러는 이런 문제를 해결할 수 없다.
그렇기에 일관성있게 서블릿 필터, 스프링 인터셉터, ControllerAdvice를 사용해서 공통으로 처리해야한다.
- 대부분의 실무에서 발생하는 예외는 DB나 네트워크 통신처럼 시스템 예외로 복구가 불가능하며 서비스나 컨트롤러는 이런 문제를 해결할 수 없다.
- 의존 관계에 대한 문제
- 컨트롤러나 서비스의 입장에서는 본인이 처리할 수 없어도 어쩔 수 없이 throws를 통해 던지는 예외를 선언해야 하므로 서비스, 컨트롤러에서 SQLException, ConnectException ... 등 특정 예외에 의존하게 되는 문제점이 발생한다.
=> 본인이 처리할 수 없는 예외를 의존해야할 뿐만 아니라 OCP 원칙에 어긋나게 된다.- ex) 레포지토리에서 JDBC를 사용하다가 JPA 기술로 변경한다면 서비스, 컨트롤러의 SQLException을 모두 JPAException에 의존하도록 고쳐야한다.
- 이 경우 만약 특정 예외에 의존하지 않게 하기 위해 throws Exception을 선언한다면 최상위 타입인 Exception을 던짐으로 모든 체크 예외를 모두 밖으로 던지는 문제가 발생한다.
=> 최상위 타입인 Exception을 던짐으로 다른 체크 예외를 체크할 수 있는 기능이 무효화되고 중요한 체크 예외를 다 놓치게 된다. 꼭 필요한 경우가 아니면 Exception 자체를 밖으로 던지는 것은 좋지 않다.
ex) 중간 중간에 특정 체크 예외가 발생해도 컴파일러 입장에서는 최상위 타입인 Exception을 던지기에 어떤 구체적인 예외가 발생할지 알 수 없고 어떤 예외는 잡고 어떤 예외는 던지는지 알 수 없기에 문법이 맞다고 판단하여 컴파일 오류가 발생하지 않는다.
- 컨트롤러나 서비스의 입장에서는 본인이 처리할 수 없어도 어쩔 수 없이 throws를 통해 던지는 예외를 선언해야 하므로 서비스, 컨트롤러에서 SQLException, ConnectException ... 등 특정 예외에 의존하게 되는 문제점이 발생한다.
- 복구 불가능한 예외
public class CheckedAppTest {
@Test
public void checked() throws Exception {
Controller controller = new Controller();
assertThatThrownBy(() -> controller.request())
.isInstanceOf(Exception.class);
}
static class Controller {
Service service = new Service();
public void request() throws SQLException, ConnectException {
service.logic();
}
}
static class Service {
Repository repository = new Repository();
NetworkClient networkClient = new NetworkClient();
public void logic() throws SQLException, ConnectException {
repository.call();
networkClient.call();
}
}
static class NetworkClient {
public void call() throws ConnectException {
throw new ConnectException("연결실패");
}
}
static class Repository {
public void call() throws SQLException {
throw new SQLException("ex");
}
}
}
- 체크 예외 테스트 코드 예제
- Repository와 NetworkClient에서 발생한 예외를 해결할 수도 없는 서비스와 컨트롤러에서 잡아서 throws 예외로 선언하고 각각 던져줬다.
- 체크 예외를 사용한 결과 예외를 해결할 수 없는 컨트롤러, 서비스에서 특정 예외에 의존하게 되는 문제가 발생한다.
언체크 예외 활용
- 이전 체크 예외 문제 상황에서 체크 예외를 런타임 예외로 변경
- 런타임 예외이기 때문에 서비스, 컨트롤러는 해당 예외를 처리할 수 없더라도 별도의 선언을 하지 않아도 된다.
public class UnCheckedAppTest {
@Test
public void unchecked() {
Controller controller = new Controller();
assertThatThrownBy(() -> controller.request())
.isInstanceOf(Exception.class);
}
static class Controller {
Service service = new Service();
public void request() {
service.logic();
}
}
static class Service {
Repository repository = new Repository();
NetworkClient networkClient = new NetworkClient();
public void logic() {
repository.call();
networkClient.call();
}
}
static class NetworkClient {
public void call() {
throw new RuntimeConnectException("연결 실패");
}
}
static class Repository {
public void call() {
try {
runSQL();
} catch (SQLException e) {
throw new RuntimeSQLException(e);
}
}
public void runSQL() throws SQLException {
throw new SQLException("ex");
}
}
static class RuntimeConnectException extends RuntimeException {
public RuntimeConnectException(String message) {
super(message);
}
}
static class RuntimeSQLException extends RuntimeException {
public RuntimeSQLException(Throwable cause) {
super(cause);
}
}
}
- 런타임 예외 테스트 코드 예제
- 체크 예외 예제와 달리 Repository에서는 발생한 체크 예외 SQLException을 내부에서 try-catch로 잡고 런타임 예외인 RuntimeSQLException으로 던져줬다.
- 기존 예외인 SQLException을 포함해줘야 예외 출력 시 스택 트레이스에서 기존 예외도 함께 확인할 수 있다.
- NetworkClient 에서도 체크 예외인 ConnectException이 아닌 런타임 예외인 RuntimeConnectException을 던져줬다.
- 런타임 예외를 사용한 결과 복구 불가능한 예외를 처리하지 못하는 서비스, 컨트롤러에서는 따로 throws 예외를 선언해주지 않아도 되기에 특정 예외에 의존하지 않을 수 있게됐다.
- 체크 예외 예제와 달리 Repository에서는 발생한 체크 예외 SQLException을 내부에서 try-catch로 잡고 런타임 예외인 RuntimeSQLException으로 던져줬다.
- 런타임 예외를 사용하면 중간에 기술이 변경되더라도 해당 예외를 사용하지 않는 서비스, 컨트롤러의 코드를 변경하지 않아도 된다.
- 예외를 공통 처리를 하는 부분만 변경하면 되기 때문에 변경의 영향 범위는 최소화된다.
- 체크 예외의 문제점으로 인해 최근 라이브러리들은 대부분 런타임 예외를 사용한다.
- 필요한 예외는 잡아서 처리하고 그렇지 않은 경우 자연스럽게 던지도록 둔다.
- 예외를 공통으로 처리하는 부분을 앞에 만들어서 처리하면 된다.
- 런타임 예외 문서화
- 런타임 예외는 놓칠 수 있기에 문서화를 잘 해야한다.
혹은 throws 런타임예외를 남겨서 중요한 예외를 인지할 수 있도록 해야한다.
- 런타임 예외는 놓칠 수 있기에 문서화를 잘 해야한다.
예외 포함과 스택 트레이스
- 예외를 전환할 때는 꼭! 기존 예외를 포함해야 한다.
그렇지 않은 경우 스택 트레이스를 확인할 때 심각한 문제가 발생한다.- 실제 발생한 예외를 확인할 수 없고 변환한 예외 이후부터 확인할 수 있는 문제가 발생한다.
- 생성자에 Throwable을 넣어줄 수 있다.
- 로그 출력을 할 때 마지막 파라미터에 예외를 넣어주면 로그에 스택 트레이스를 출력할 수 있다.
- ex) log.info("message={}", e, ex)
- System.out에 스택 트레이스를 출력하려면 e.printStackTrace()를 사용하면 되지만 실무에서는 항상 로그를 사용해야 한다.
정리
- 예외
- 체크 예외 : RuntimeException을 제외한 Exception의 모든 자식 타입을 의미하며 컴파일러가 체크하는 예외이다.
- 언체크(런타임) 예외 : RuntimeException과 하위 타입을 의미하며 컴파일러가 체크하지 않는 예외이다.
- 예외의 기본 규칙 : 예외 발생 시 잡아서 처리하거나 처리하지 못한다면 밖으로 던져야한다.
- 체크 예외는 처리하지 못한다면 무조건 throws 예외로 선언해서 밖으로 던져야 한다.
- 언체크 예외는 처리하지 못하더라도 throws를 생략할 수 있으며 이 경우 자동으로 밖으로 던져진다.
- 예외 활용 기본 원칙
- 기본적으로 런타임 예외를 사용할 것
- 런타임 예외 사용 시 처리하지 못하더라도 throws를 생략가능하기에 특정 예외에 의존하지 않아도 된다.
- 런타임 예외는 놓칠 수 있기에 문서화를 잘 하거나 코드로 명시해줘야 한다.
- 체크 예외는 비지니스 로직상 의도적으로 던지는 예외면서 해당 예외를 잡아서 반드시 처리해야하는 문제일 때만 체크 예외 사용
- 기본적으로 런타임 예외를 사용할 것
- 대부분의 예외는 복구할 수 없고 공통 처리 부분(필터 / 인터셉터 / ControllerAdvice)에서 처리하도록 하며 런타임 예외를 사용해야 한다.
- 체크 예외를 체크할 수 있는 기능이 무효화되고 중요한 체크 예외를 다 놓치게 되기에 특정 예외에 의존하지 않게 하기 위해 throws Exception은 사용하지 말자
- 예외를 전환할 때는 꼭 기존 예외를 포함해야 한다!!
출처 : [인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard
스프링 DB 1편 - 데이터 접근 핵심 원리 강의 | 김영한 - 인프런
김영한 | 백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니
www.inflearn.com
'Spring > [인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리]' 카테고리의 다른 글
[인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리] 스프링과 문제 해결 - 예외 처리, 반복 (0) | 2024.11.19 |
---|---|
[인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리] 스프링과 문제 해결 - 트랜잭션 (2) | 2024.11.17 |
[인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리] 트랜잭션 - 개념 이해 (2) | 2024.11.14 |
[인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리] 커넥션풀과 데이터소스 이해 (0) | 2024.11.14 |
[인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리] JDBC 이해 (0) | 2024.11.11 |