1. 예외 계층
스프링이 제공하는 예외 추상화를 이해하려면 먼저 자바 기본 예외 체계를 알아야 합니다. 예외는 자바 언어의 기본 문법이지만, 실무에서는 체크 예외와 언체크 예외를 언제 어떻게 사용할 것인가가 매우 중요합니다.
예외 계층 그림

계층별 의미
- Object: 예외도 객체입니다. 모든 객체의 최상위 부모는 Object이므로 예외의 최상위 부모도 Object입니다.
- Throwable: 자바 예외 계층의 최상위 타입입니다. 하위에 Exception과 Error가 있습니다.
- Error: 메모리 부족, JVM 내부 오류처럼 애플리케이션에서 복구 불가능한 시스템 예외입니다. 애플리케이션 개발자는 보통 이 예외를 잡으려고 하면 안 됩니다.
- Exception: 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외입니다. Exception과 그 하위 예외는 기본적으로 컴파일러가 체크하는 체크 예외입니다.
- RuntimeException: 컴파일러가 체크하지 않는 언체크 예외입니다. RuntimeException과 그 자식 예외는 모두 언체크 예외이며, 보통 런타임 예외라고 부릅니다.
왜 Throwable을 잡으면 안 되는가?
상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡습니다. 따라서 Throwable을 잡으면 Exception뿐만 아니라 복구 불가능한 Error까지 잡히게 됩니다.
try {
logic();
} catch (Throwable e) {
// Error까지 잡을 수 있으므로 애플리케이션 로직에서는 피하는 것이 좋다.
}
애플리케이션 로직에서는 보통 Exception부터 필요한 예외로 생각하고 잡으면 됩니다.
2. 예외 기본 규칙
예외는 폭탄 돌리기와 같습니다. 예외가 발생하면 현재 위치에서 잡아서 처리하거나, 처리할 수 없다면 호출한 쪽으로 던져야 합니다.
예외 처리

5번 위치에서 예외를 잡아서 처리하면, 그 이후에는 애플리케이션 로직이 정상 흐름으로 계속 동작할 수 있습니다.
예외 던짐

예외를 처리하지 못하면 호출한 곳으로 예외가 계속 던져집니다. 결국 예외를 처리할 수 있는 지점까지 올라가거나, 끝까지 처리되지 않으면 시스템/프레임워크가 처리합니다.
예외의 2가지 기본 규칙
- 예외는 잡아서 처리하거나 던져야 합니다.
- 예외를 잡거나 던질 때 지정한 예외뿐 아니라 그 예외의 자식 예외도 함께 처리됩니다.
상위 타입으로 잡는 경우
try {
repository.call();
} catch (Exception e) {
// Exception 하위 예외를 모두 잡는다.
}
Exception을 잡으면 Exception의 하위 예외도 모두 잡을 수 있습니다. 다만 너무 넓은 범위로 잡으면 의도하지 않은 예외까지 잡을 수 있으므로 주의해야 합니다.
상위 타입으로 던지는 경우
public void call() throws Exception {
repository.call();
}
throws Exception으로 선언하면 Exception 하위 예외를 모두 던질 수 있습니다. 하지만 어떤 예외가 발생할 수 있는지 불명확해지고, 체크 예외의 장점이 약해질 수 있습니다.
예외를 끝까지 처리하지 못하면?
- 자바 main() 쓰레드: 예외 로그를 출력하면서 시스템이 종료됩니다.
- 웹 애플리케이션: 하나의 요청에서 예외가 발생했다고 전체 서버가 종료되면 안 됩니다. WAS가 예외를 받아 처리하고, 보통 오류 페이지나 HTTP 500 응답을 내려줍니다.
3. 체크 예외 기본 이해
Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외입니다. 단, RuntimeException은 예외입니다.
체크 예외는 잡아서 처리하거나, 밖으로 던진다고 선언하거나 둘 중 하나를 반드시 해야 합니다.
CheckedTest 전체 코드
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 CheckedTest {
@Test
void checked_catch() {
Service service = new Service();
service.callCatch();
}
@Test
void checked_throw() {
Service service = new Service();
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");
}
}
}
Exception을 상속받으면 체크 예외가 됨
static class MyCheckedException extends Exception {
public MyCheckedException(String message) {
super(message);
}
}
MyCheckedException은 Exception을 상속받았으므로 체크 예외가 됩니다. 예외 메시지를 보관하고 싶으면 생성자에서 super(message)를 호출하면 됩니다.
체크 예외를 잡아서 처리하는 코드
@Test
void checked_catch() {
Service service = new Service();
service.callCatch();
}
public void callCatch() {
try {
repository.call();
} catch (MyCheckedException e) {
// 예외 처리 로직
log.info("예외 처리, message={}", e.getMessage(), e);
}
}
실행 순서 분석
- test → service.callCatch() → repository.call() 호출
- repository.call()에서 MyCheckedException 발생 후 던짐
- service.callCatch()의 catch 블록에서 예외 처리
- 예외가 테스트 메서드까지 올라오지 않고 정상 흐름으로 종료
실행 결과 로그
[Test worker] INFO hello.jdbc.exception.basic.CheckedTest - 예외 처리, message=ex
hello.jdbc.exception.basic.CheckedTest$MyCheckedException: ex
at hello.jdbc.exception.basic.CheckedTest$Repository.call(CheckedTest.java:64)
at hello.jdbc.exception.basic.CheckedTest$Service.callCatch(CheckedTest.java:45)
at hello.jdbc.exception.basic.CheckedTest.checked_catch(CheckedTest.java:14)
여기서 첫 줄은 우리가 직접 남긴 로그입니다. 두 번째 줄부터는 예외 객체를 로그 마지막 인수로 전달했기 때문에 출력된 스택 트레이스입니다.
log.info("예외 처리, message={}", e.getMessage(), e);
마지막 인수 e가 있어야 스택 트레이스가 로그에 함께 출력됩니다.
catch는 해당 타입과 하위 타입을 모두 잡음
public void callCatch() {
try {
repository.call();
} catch (Exception e) {
// 예외 처리 로직
}
}
catch (Exception e)는 MyCheckedException도 잡을 수 있습니다. 하지만 정확하게 MyCheckedException만 잡고 싶다면 구체 타입을 적어주는 것이 좋습니다.
4. 체크 예외를 밖으로 던지는 경우
현재 계층에서 체크 예외를 처리할 수 없다면 throws를 사용해서 밖으로 던져야 합니다.
테스트 코드
@Test
void checked_throw() {
Service service = new Service();
assertThatThrownBy(() -> service.callThrow())
.isInstanceOf(MyCheckedException.class);
}
던지는 코드
public void callThrow() throws MyCheckedException {
repository.call();
}
실행 순서 분석
- test → service.callThrow() → repository.call() 호출
- repository.call()에서 예외 발생 후 던짐
- service.callThrow()는 예외를 처리하지 않고 다시 던짐
- 테스트 메서드까지 예외가 도착
- assertThatThrownBy가 MyCheckedException 발생을 확인하면 테스트 성공
throws를 선언하지 않으면 컴파일 오류
public void callThrow() {
repository.call();
}
체크 예외를 처리하지도 않고 throws로 선언하지도 않으면 컴파일 오류가 발생합니다.
Unhandled exception: hello.jdbc.exception.basic.CheckedTest.MyCheckedException
상위 타입으로 던질 수도 있음
public void callThrow() throws Exception {
repository.call();
}
throws Exception으로 선언해도 MyCheckedException을 던질 수 있습니다. 하지만 너무 넓은 타입을 던지면 어떤 예외가 발생할 수 있는지 알기 어려워지고, 체크 예외의 장점이 약해집니다.
체크 예외의 장단점
- 장점: 개발자가 실수로 예외 처리를 누락하지 않도록 컴파일러가 잡아줍니다.
- 단점: 처리할 수 없는 예외까지 모두 잡거나 던져야 해서 코드가 번거로워집니다. 또한 서비스/컨트롤러가 하위 기술 예외에 의존하게 되는 문제가 생길 수 있습니다.
5. 언체크 예외 기본 이해
RuntimeException과 그 하위 예외는 언체크 예외입니다. 언체크 예외는 컴파일러가 예외 처리 여부를 체크하지 않습니다.
체크 예외 VS 언체크 예외
| 구분 | 처리 방식 | 특징 |
| 체크 예외 | 잡지 않으면 throws 필수 | 컴파일러가 누락을 잡아줌 |
| 언체크 예외 | 잡지 않아도 throws 생략 가능 | 처리할 수 없는 예외를 자연스럽게 전파 가능 |
UncheckedTest 전체 코드
package hello.jdbc.exception.basic;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@Slf4j
public 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();
/**
* 필요한 경우 예외를 잡아서 처리하면 된다.
*/
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");
}
}
}
RuntimeException을 상속받으면 언체크 예외가 됨
static class MyUncheckedException extends RuntimeException {
public MyUncheckedException(String message) {
super(message);
}
}
RuntimeException을 상속받으면 컴파일러가 체크하지 않는 언체크 예외가 됩니다.
언체크 예외를 잡아서 처리하는 코드
try {
repository.call();
} catch (MyUncheckedException e) {
// 예외 처리 로직
log.info("error", e);
}
언체크 예외도 필요한 경우에는 이렇게 잡아서 처리할 수 있습니다.
언체크 예외를 밖으로 던지는 코드 - 생략
public void callThrow() {
repository.call();
}
언체크 예외는 체크 예외와 달리 throws 예외를 선언하지 않아도 자동으로 밖으로 던져집니다.
언체크 예외를 밖으로 던지는 코드 - 선언
public void callThrow() throws MyUncheckedException {
repository.call();
}
언체크 예외도 throws를 선언할 수 있습니다. 필수는 아니지만, 중요한 예외라면 IDE나 문서를 통해 호출자가 인지할 수 있게 해주는 효과가 있습니다.
언체크 예외의 장단점
- 장점: 처리할 수 없는 예외를 매번 throws로 선언하지 않아도 됩니다. 따라서 불필요한 예외 의존관계를 줄일 수 있습니다.
- 단점: 컴파일러가 강제하지 않으므로 개발자가 예외를 놓칠 수 있습니다. 그래서 문서화가 중요합니다.
6. 체크 예외 활용
실무에서는 다음 원칙을 기억하면 됩니다.
- 기본적으로 언체크 런타임 예외를 사용합니다.
- 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용합니다.
- 호출자가 반드시 잡아서 처리해야 하는 문제일 때 체크 예외를 고려합니다.
체크 예외를 사용할 수 있는 예
- 계좌 이체 실패 예외
- 결제 시 포인트 부족 예외
- 로그인 ID/PW 불일치 예외
이런 예외는 비즈니스 흐름에서 의미가 있고, 호출자가 잡아서 사용자에게 적절한 안내를 해야 할 수 있습니다.
7. 체크 예외의 문제점
체크 예외는 컴파일러가 예외 누락을 잡아주는 장점이 있지만, 시스템 예외에 남용하면 큰 문제가 생깁니다.
체크 예외 문제점 - 그림

리포지토리는 DB 접근 중 SQLException을 던지고, NetworkClient는 외부 네트워크 연결 중 ConnectException을 던진다고 가정합니다. 서비스는 두 컴포넌트를 모두 호출하므로 두 체크 예외를 모두 처리해야 합니다.
그런데 서비스는 보통 SQLException이나 ConnectException을 직접 해결할 방법이 없습니다. 컨트롤러도 마찬가지입니다.
CheckedAppTest 코드
package hello.jdbc.exception.basic;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.net.ConnectException;
import java.sql.SQLException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@Slf4j
public class CheckedAppTest {
@Test
void checked() {
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");
}
}
}
문제 1. 복구 불가능한 예외
대부분의 시스템 예외는 복구가 불가능합니다. 예를 들어 SQLException은 SQL 문법 오류, DB 서버 장애, 연결 문제 등으로 발생할 수 있습니다. 서비스나 컨트롤러가 이런 문제를 직접 해결하기는 어렵습니다.
이런 예외는 보통 다음과 같이 공통으로 처리합니다.
- 오류 로그를 남김
- 개발자가 빠르게 인지할 수 있도록 알림 발송
- 사용자에게는 “서비스에 문제가 있습니다.” 같은 일반적인 메시지 제공
- API라면 HTTP 500 내부 서버 오류 응답
스프링에서는 서블릿 필터, 인터셉터, ControllerAdvice 등을 사용해서 이런 공통 예외 처리를 구성할 수 있습니다.
문제 2. 의존 관계 문제
class Controller {
public void request() throws SQLException, ConnectException {
service.logic();
}
}
class Service {
public void logic() throws SQLException, ConnectException {
repository.call();
networkClient.call();
}
}
서비스와 컨트롤러가 java.sql.SQLException에 의존하게 됩니다. 즉, 서비스 계층이 JDBC라는 구현 기술의 예외를 알아야 합니다.
체크 예외 구현 기술 변경 시 파급 효과

만약 리포지토리 구현 기술을 JDBC에서 JPA로 바꾸면 SQLException 대신 다른 예외가 발생할 수 있습니다. 이 경우 서비스/컨트롤러의 throws SQLException 선언도 함께 바꿔야 합니다.
// JDBC 사용 시
public void logic() throws SQLException {
repository.call();
}
// JPA로 변경하면 예외 타입도 변경될 수 있음
public void logic() throws JPAException {
repository.call();
}
결국 OCP, DI를 통해 구현체를 바꿔도 클라이언트 코드는 유지하고 싶다는 장점이 체크 예외 때문에 깨질 수 있습니다.
throws Exception의 유혹과 문제
void method() throws Exception {
// ...
}
SQLException, ConnectException을 일일이 적기 귀찮아서 throws Exception을 사용할 수도 있습니다. 하지만 이렇게 하면 모든 체크 예외를 다 밖으로 던진다는 뜻이 됩니다.
문제는 컴파일러가 중요한 체크 예외를 더 이상 구분해서 잡아주지 못한다는 점입니다. 결과적으로 체크 예외를 의도한 대로 사용하는 것이 아닙니다.
8. 언체크 예외 활용
복구 불가능한 시스템 예외는 체크 예외로 계속 노출하기보다 런타임 예외로 전환해서 사용하는 것이 실무적으로 더 적합합니다.
런타임 예외 사용 - 그림

SQLException을 런타임 예외인 RuntimeSQLException으로 전환하고, ConnectException도 RuntimeConnectException 같은 런타임 예외로 전환합니다. 그러면 서비스/컨트롤러는 처리할 수 없는 예외를 강제로 선언하지 않아도 됩니다.
UncheckedAppTest 코드
package hello.jdbc.exception.basic;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@Slf4j
public class UncheckedAppTest {
@Test
void unchecked() {
Controller controller = new Controller();
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() {
}
public RuntimeSQLException(Throwable cause) {
super(cause);
}
}
}
예외 전환
static class Repository {
public void call() {
try {
runSQL();
} catch (SQLException e) {
throw new RuntimeSQLException(e);
}
}
private void runSQL() throws SQLException {
throw new SQLException("ex");
}
}
리포지토리에서 체크 예외인 SQLException이 발생하면 런타임 예외인 RuntimeSQLException으로 바꿔서 던집니다. 이때 기존 예외 e를 반드시 포함해야 합니다.
런타임 예외 - 대부분 복구 불가능한 예외 처리에 적합
시스템에서 발생하는 예외는 대부분 서비스/컨트롤러에서 복구할 수 없습니다. 런타임 예외를 사용하면 이런 복구 불가능한 예외를 각 계층이 강제로 의존하지 않아도 됩니다.
런타임 예외 throws 생략
class Controller {
public void request() {
service.logic();
}
}
class Service {
public void logic() {
repository.call();
networkClient.call();
}
}
런타임 예외이므로 컨트롤러와 서비스는 throws RuntimeSQLException, RuntimeConnectException을 생략할 수 있습니다. 덕분에 서비스/컨트롤러가 하위 구현 기술 예외에 직접 의존하지 않습니다.
런타임 예외 구현 기술 변경 시 파급 효과

런타임 예외를 사용하면 구현 기술이 변경되어도 해당 예외를 직접 사용하지 않는 서비스/컨트롤러 코드는 변경하지 않아도 됩니다. 공통 예외 처리 계층에서 필요한 경우만 변경하면 됩니다.
9. 런타임 예외는 문서화가 중요함
런타임 예외는 컴파일러가 강제하지 않기 때문에 문서화가 중요합니다. 중요한 런타임 예외라면 Javadoc이나 throws 선언을 통해 개발자가 인지할 수 있게 해주는 것이 좋습니다.
JPA EntityManager 예시
/**
* Make an instance managed and persistent.
* @param entity entity instance
* @throws EntityExistsException if the entity already exists.
* @throws IllegalArgumentException if the instance is not an
* entity
* @throws TransactionRequiredException if there is no transaction when
* invoked on a container-managed entity manager of that is of type
* <code>PersistenceContextType.TRANSACTION</code>
*/
public void persist(Object entity);
스프링 JdbcTemplate 예시
/**
* Issue a single SQL execute, typically a DDL statement.
* @param sql static SQL to execute
* @throws DataAccessException if there is any problem
*/
void execute(String sql) throws DataAccessException;
DataAccessException은 런타임 예외입니다. 그래도 문서와 코드에 명시하면 호출자가 IDE나 문서를 통해 예외 발생 가능성을 인지할 수 있습니다.
10. 예외 포함과 스택 트레이스
예외를 전환할 때는 반드시 기존 예외를 포함해야 합니다. 그렇지 않으면 실제 장애 원인을 찾기 어려워집니다.
로그 출력 코드
@Test
void printEx() {
Controller controller = new Controller();
try {
controller.request();
} catch (Exception e) {
// e.printStackTrace();
log.info("ex", e);
}
}
로그를 출력할 때 마지막 파라미터에 예외를 넣어주면 스택 트레이스가 출력됩니다.
log.info("message={}", "message", ex);
log.info("ex", ex);
실무에서는 System.out이나 e.printStackTrace()보다 로그를 사용해야 합니다.
기존 예외를 포함하는 경우
public void call() {
try {
runSQL();
} catch (SQLException e) {
throw new RuntimeSQLException(e); // 기존 예외(e) 포함
}
}
실행 로그
13:10:45.626 [Test worker] INFO hello.jdbc.exception.basic.UncheckedAppTest - ex
hello.jdbc.exception.basic.UncheckedAppTest$RuntimeSQLException: java.sql.SQLException: ex
at hello.jdbc.exception.basic.UncheckedAppTest$Repository.call(UncheckedAppTest.java:61)
at hello.jdbc.exception.basic.UncheckedAppTest$Service.logic(UncheckedAppTest.java:45)
at hello.jdbc.exception.basic.UncheckedAppTest$Controller.request(UncheckedAppTest.java:35)
at hello.jdbc.exception.basic.UncheckedAppTest.printEx(UncheckedAppTest.java:24)
Caused by: java.sql.SQLException: ex
at hello.jdbc.exception.basic.UncheckedAppTest$Repository.runSQL(UncheckedAppTest.java:66)
at hello.jdbc.exception.basic.UncheckedAppTest$Repository.call(UncheckedAppTest.java:59)
이 경우 변환된 RuntimeSQLException뿐 아니라 원인 예외인 SQLException의 위치까지 확인할 수 있습니다.
기존 예외를 포함하지 않는 경우
public void call() {
try {
runSQL();
} catch (SQLException e) {
throw new RuntimeSQLException(); // 기존 예외(e) 제외
}
}
실행 로그
[Test worker] INFO hello.jdbc.exception.basic.UncheckedAppTest - ex
hello.jdbc.exception.basic.UncheckedAppTest$RuntimeSQLException: null
at hello.jdbc.exception.basic.UncheckedAppTest$Repository.call(UncheckedAppTest.java:61)
at hello.jdbc.exception.basic.UncheckedAppTest$Service.logic(UncheckedAppTest.java:45)
이 경우 기존에 발생한 SQLException과 그 스택 트레이스를 확인할 수 없습니다. 실제 DB 연동 상황이라면 DB에서 발생한 원인을 잃어버리는 심각한 문제가 됩니다.
⚠️ 예외를 전환할 때는 꼭 기존 예외를 포함해야 합니다. 즉, throw new RuntimeSQLException(e)처럼 원인 예외를 생성자에 넘겨야 합니다.
11. 최종 요약 정리
| 주제 | 핵심 정리 |
| 예외 계층 | Throwable 아래에 Exception과 Error가 있으며, 애플리케이션 로직은 보통 Exception부터 다룸 |
| 체크 예외 | 컴파일러가 예외 처리 여부를 강제함. 잡거나 throws로 선언해야 함 |
| 언체크 예외 | RuntimeException 하위 예외. 잡지 않아도 throws 선언 생략 가능 |
| 체크 예외 장점 | 컴파일러가 예외 누락을 잡아주는 안전장치 역할 |
| 체크 예외 단점 | 복구 불가능한 예외까지 강제 선언해야 하고, 구현 기술 예외에 의존하게 됨 |
| 언체크 예외 장점 | 처리할 수 없는 예외를 자연스럽게 전파하고, 서비스/컨트롤러의 기술 의존성을 줄임 |
| 체크 예외 사용처 | 비즈니스적으로 호출자가 반드시 처리해야 하는 예외에 제한적으로 사용 |
| 예외 전환 | 체크 시스템 예외를 런타임 예외로 전환하되, 반드시 기존 예외를 포함해야 함 |
| 문서화 | 런타임 예외는 컴파일러가 강제하지 않으므로 Javadoc 또는 throws 선언으로 문서화하는 것이 좋음 |
실무 관점 핵심 문장
- 기본적으로는 언체크 런타임 예외를 사용합니다.
- 체크 예외는 비즈니스적으로 반드시 처리해야 하는 경우에만 제한적으로 사용합니다.
- SQLException, 네트워크 오류처럼 대부분 복구 불가능한 시스템 예외는 런타임 예외로 전환하고, 공통 예외 처리에서 로그/알림/오류 응답을 처리합니다.
- 예외 전환 시 기존 예외를 포함하지 않으면 원인 스택 트레이스를 잃어버리므로 반드시 throw new RuntimeException(e) 형태로 원인 예외를 포함해야 합니다.
'INFLEARN' 카테고리의 다른 글
| [스프링 DB 2편 - 데이터 접근 활용 기술] 1. 데이터 접근 기술 - 시작 (0) | 2026.07.12 |
|---|---|
| [스프링 DB 1편 - 데이터 접근 핵심 원리] 6. 스프링과 문제 해결 - 예외 처리, 반복 (0) | 2026.07.10 |
| [스프링 DB 1편 - 데이터 접근 핵심 원리] 4. 스프링과 문제 해결 - 트랜잭션 (0) | 2026.07.09 |
| [스프링 DB 1편 - 데이터 접근 핵심 원리] 3. 트랜잭션 이해 (0) | 2026.07.08 |
| [스프링 DB 1편 - 데이터 접근 핵심 원리] 2. 커넥션풀과 데이터소스 이해 (0) | 2026.07.07 |