1. 예외 계층
- Object : 예외의 최상위 부모도 Object
- Throwable : 최상위 예외 - 하위에 Exception과 Error가 있음. 이 예외는 잡으면 안됨.
- Error : 메모리 부족이나 심각한 시스템 오류같이 애플리케이션에서 복구 불가능한 시스템 예외. 이 예외는 잡으면 안됨. (처리할 수 X)
- 상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡음.
-> Throwable 예외를 잡으면 안되는 이유 -> Error 예외까지 잡아버릴 수 있기 때문 (잡으면 안되는데)
-> Exception 부터 잡아야 함 - Error도 언체크 예외
- 상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡음.
- Exception : 체크 예외
- 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외
- Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외
단, RuntimeException은 아님
- RuntimeException : 언체크 예외 (= 런타임 예외)
- RuntimeException과 그 하위 예외는 모두 컴파일러가 체크하지 않는 언체크 예외
예외 처리 2가지
- 잡아서 처리 (그 자식 예외도 모두 잡음)
- 던짐 (그 자식 예외도 모두 던짐) -> 처리하지 못하고 계속 던질 때는 오류 페이지
2. 체크 예외 (Exception)
- Exception을 상속받으면 '체크 예외'
- Exception과 그 하위 예외는 모두 컴파일러가 체크하는 '체크 예외'
단, RuntimeException은 예외 - 체크예외는 잡아서 처리하거나, 밖으로 던지도록 선언하거나 둘중 하나 필수
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 예외 선언 필수
*/
public void callThrow() throws MyCheckedException {
repository.call();
}
}
/**
* 예외 발생
*/
static class Repository {
public void call() throws MyCheckedException {
throw new MyCheckedException("ex");
}
}
}
(1) 예외를 잡아서 처리
@Test
void checked_catch() {
Service service = new Service();
service.callCatch();
}
/**
* 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);
}
}
}
/**
* 예외 발생
*/
static class Repository {
public void call() throws MyCheckedException {
throw new MyCheckedException("ex");
}
}
실행 순서
- test -> service.callCatch() -> repository.call() [예외 발생, 던짐]
- repository.call() -> service.callCatch() [예외 잡아서 처리] -> test
- service.callCatch()에서 예외를 잡아서 처리했기 때문에 테스트까지 예외가 올라오지 않음
- catch (Exception e) 상위타입 Exception을 넣어도 잡을 수 있음 -> 하위 타입까지 모두 잡음
(2) 예외를 밖으로 던짐
@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();
/**
* 체크 예외를 밖으로 던짐 - throws 예외 선언 필수
*/
public void callThrow() throws MyCheckedException {
repository.call();
}
}
/**
* 예외 발생
*/
static class Repository {
public void call() throws MyCheckedException {
throw new MyCheckedException("ex");
}
}
실행 순서
- test -> service.callThrow() -> repository.call() [ 예외 발생, 던짐 ]
- repository.call() -> service.callThrow() [ 예외 던짐 ] -> test [ 예외 도착 ]
- service.callThrow() 에서 예외를 처리하지 않고, 밖으로 던짐 -> 테스트까지 예외가 올라옴
- 던질 때도 상위 타입인 Exception을 던질 수 있음
- 체크 예외를 처리할 수 없을 때는 "throws 예외" 를 사용해서 밖으로 던질 예외를 필수로 지정 !
(던지지 않으면 컴파일 오류)- 장점 : 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아줌
- 단점 : 모든 체크 예외를 잡거나 던져야 함 -> 번거로움
3. 언체크 예외 RuntimeException
- RuntimeException을 상속받으면 '언체크 예외'
- 언체크 예외는 컴파일러가 예외를 체크하지 않음
- 장점 : 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있음
- 단점 : 실수로 예외를 누락할 수 있음
- 체크 예외 vs 언체크 예외
- 체크 예외 : 예외를 잡아서 처리하지 않으면, 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);
}
}
/**
* UnChecked 예외는 예외를 잡거나, 던지지는 않아도 됨
* -> 예외를 잡지 않으면 자동으로 던짐
*/
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");
}
}
}
언체크 예외를 잡아서 처리
try {
repository.call();
} catch (MyUnCheckedException e) {
// 예외처리 로직
log.info("예외 처리, message={}", e.getMessage(), e);
}
언체크 예외를 밖으로 던짐 - throws 선언 생략 -> 컴파일러가 체크하지 않음
public void callThrow() {
repository.call();
}
언체크 예외를 밖으로 던짐 - throws 선언
public void callThrow() throws MyUncheckedException {
repository.call();
}
4. 체크 예외 활용 - 체크 예외의 문제점
📌 기본적으로 언체크(런타임) 예외 사용
📌 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용
(예- 계좌 이체 실패 예외, 결제시 포인트 부족 예외, 로그인 아이디, 패스워드 불일치 예외)
1) 체크 예외의 문제점 - 그림
- 리포지토리(DB 접근)에서는 SQLException 체크 예외,
NetworkClient(외부 네트워크 접속)에서는 ConnectException 체크 예외를 던지는 경우 - 서비스에서 리포지토리, NetworkClient를 둘다 호출
- 체크 예외인 SQLException, ConnectException 을 처리해야 함
- 서비스는 이 둘을 처리할 수 X (-> 데이터베이스 문제 / 네트워크 연결 실패 라서)
- 밖으로 던짐 -> throws SQLException, ConnectException
- 컨트롤러도 두 예외를 처리할 수 X
- 밖으로 던짐 -> throws SQLException, ConnectException
- 밖으로 던짐 -> throws SQLException, ConnectException
- 웹 애플리케이션은 서블릿의 오류 페이지 또는 스프링MVC가 제공하는 ControllerAdvice에서 이런 예외를 공통으로 처리해줌
- 이렇게 해결 불가능한 공통 예외는 별도의 오류 로그를 남기고, 개발자가 오류를 빨리 인지할 수 있도록 전달
2) 체크 예외의 문제점 - 코드
package hello.jdbc.exception.basic;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import java.net.ConnectException;
import java.sql.SQLException;
public class CheckedAppTest {
@Test
void checked() {
Controller controller = new Controller();
Assertions.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");
}
}
}
- 서비스
- 해당 체크 예외 처리 X -> logic() throws SQLException, ConnectException 선언
- 컨트롤러
- 해당 체크 예외 처리 X -> request() throws SQLException, ConnectException 선언
3) 체크 예외의 문제점 정리
- 복구 불가능한 예외
- 데이터베이스나 시스템에서 올라온 예외는 대부분 복구 불가능
-> 서비스나 컨트롤러가 해결할 수 없음
-> 일관성 있는 공통 처리 필요
-> 오류 로그 남기고 개발자에게 빨리 인지시키기
-> 서블릿 필터, 스프링 인터셉터, 스프링의 ControllerAdvice 사용
- 데이터베이스나 시스템에서 올라온 예외는 대부분 복구 불가능
- 의존 관계에 대한 문제
- 처리할 수 없어도 체크 예외이기 때문에 서비스, 컨트롤러에서 일단 받아서 throws 로 던지는 예외 선언 필요
- throws SQLException, ConnectException
-> 서비스, 컨트롤러에서 java.sql.SQLException (JDBC)을 의존하는 것 - JDBC를 JPA로 변경한다면, SQLException 코드를 모두 JPA에 의존하도록 고쳐야 함
- 의존관계를 없애기 위해 throws Exception 으로 상위 예외를 던지면
-> 코드는 깔끔해지는 것 같지만, 중요한 체크 예외까지 다 던져버림
-> 잡아서 해결할 수가 없음
5. 언체크(런타임) 예외 활용
1) 런타임 예외 사용 - 그림
- SQLException 체크 예외를 -> RuntimeSQLException 런타임 예외로 변환
- ConnectException 체크 예외를 -> RuntimeConnectException 런타임 예외로 변환
- 런타임 예외 -> 서비스, 컨트롤러는 해당 예외를 처리할 수 없으면 별도의 선언 없이 그냥 두면 됨
2) 런타임 예외 사용 - 코드
package hello.jdbc.exception.basic;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import java.net.ConnectException;
import java.sql.SQLException;
@Slf4j
public class UnCheckedAppTest {
@Test
void unchecked() {
Controller controller = new Controller();
Assertions.assertThatThrownBy(() -> controller.request())
.isInstanceOf(Exception.class);
}
@Test
void printEx() {
Controller controller = new Controller();
try {
controller.request();
} catch (Exception e) {
// e.printStackTrace();
log.info("ex", e);
}
}
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);
}
}
private 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);
}
}
}
예외 전환
- 리포지토리에서 SQLException 체크 예외 발생
-> 잡아서 RuntimeSQLException 런타임 예외로 전환
(이때 RuntimeSQLException(e) 로 기존 예외 포함해야 예외 출력시 스택 트레이스에서 기존 예외도 확인할 수 있음!) - NetworkClient에서 단순히 기존 체크 예외를 RuntimeConnectException 런타임 예외 발생하도록 함
런타임 예외
- 복구 불가능한 예외
- 서비스나 컨트롤러에서 신경 X -> 일관성있게 공통으로 처리
- 서비스나 컨트롤러에서 신경 X -> 일관성있게 공통으로 처리
- 의존 관계
- 해당 객체가 처리할 수 없으면 그냥 냅둠 -> 위로 올라감
- throws 선언 생략 -> 의존 관계 발생 X
- 중간에 기술이 변경되어도 해당 예외를 사용하지 않는 컨트롤러, 서비스에서 코드 변경할 필요 X
공통으로 처리하는 한곳만 변경하면 됨
- 문서화
- 런타임 예외는 놓칠 수 있기 때문에 문서화 중요
원래 - 체크 예외 선호 -> 그래서 자바는 기본적으로 체크 예외 제공이 많음
최근 - 런타임 예외 선호 -> JPA, 스프링은 대부분 런타임 예외 제공
6. 예외 포함과 스택 트레이스
📌 예외를 전환할 때 -> 반드시 기존 예외 포함해야 함
1) 런타임 예외 생성
// 런타임 예외 생성
static class RuntimeSQLException extends RuntimeException {
public RuntimeSQLException(Throwable cause) {
super(cause);
}
}
2) 로그에 스택 트레이스 출력
@Test
void printEx() {
Controller controller = new Controller();
try {
controller.request();
} catch (Exception e) {
// e.printStackTrace();
log.info("ex", e);
}
}
- 로그 출력 시 마지막 파라미터에 예외를 넣어주면 -> 로그에 stack trace 출력
- System.out에 스택 트레이스 출력하려면 -> e.printStackTrace() 사용 ( -> 비추 -> 로그 사용 )
(1) 기존 예외 포함
public void call() {
try {
runSQL();
} catch (SQLException e) {
// 예외 전환 (기존 예외 포함해서 던짐)
throw new RuntimeSQLException(e);
}
}
-> 기존에 발생한 SQLException과 스택 트레이스 확인 가능 (기존 예외인 SQL Exception에 대한 정보)
-> DB에서 발생한 원인을 알 수 있음
(2) 기존 예외 포함 X
public void call() {
try {
runSQL();
} catch (SQLException e) {
// 예외 전환 (기존 예외(e) 제외)
throw new RuntimeSQLException();
}
}
-> 기존 예외인 SQLException에 대한 정보를 확인할 수 X
-> 변환한 RuntimeSQLException부터 예외 확인 가능
-> 예외의 원인을 알 수 X
'Spring' 카테고리의 다른 글
[인프런/스프링 DB 2편] 1. 데이터 접근 기술 (0) | 2023.06.28 |
---|---|
[인프런/스프링 DB 1편] 6. 스프링과 문제 해결 - 예외 처리, 반복 (0) | 2023.06.26 |
[인프런/스프링 DB 1편] 4. 스프링과 문제 해결 - 트랜잭션 (0) | 2023.06.21 |
[인프런/스프링 DB 1편] 3. 트랜잭션 (0) | 2023.06.20 |
[인프런/스프링 DB 1편] 2. 커넥션풀과 데이터소스 (0) | 2023.06.19 |