4. Exception
4장. 예외
4.1. 사라진 SQLException
3장에서 JdbcContext를 사용하던 코드가 4장에서 JdbcTemplate으로 바뀌면서 메서드 선언부에 있던 throws SQLException이 사라집니다.
JdbcContext 사용 시
fun deleteAll() {
jdbcContext.executeSql("delete from users")
}JdbcContext 내부에서는 JDBC API를 직접 사용하므로, 실제로는 SQLException이 발생할 수 있고 이를 메서드 밖으로 던지고 있었습니다.
JdbcTemplate 적용 후
fun deleteAll() {
jdbcTemplate.update("delete from users")
}JdbcTemplate을 사용하는 순간, 메서드 시그니처에서 SQLException은 완전히 사라집니다.
SQLException은 다음과 같은 특징을 가집니다.
SQL 문법 오류
DB 서버 다운
네트워크 장애
커넥션 풀 고갈
이 중 애플리케이션 코드에서 복구 가능한 경우는 거의 없습니다. 그럼에도 불구하고 체크 예외이기 때문에 모든 호출자는 이를 처리해야 합니다.
결과적으로 개발자는 다음 중 하나를 선택하게 됩니다.
의미 없는 catch
무의미한 throws 전파
JdbcTemplate은 이 문제를 예외 전환(exception translation) 으로 해결합니다.
SQLException을 그대로 노출하지 않음
DB 벤더, 에러 코드에 따라 의미 있는 예외로 변환
모두
RuntimeException계열로 통일
즉, SQLException이 사라진 이유는
예외를 숨겨서가 아니라, 잘못 쓰이지 않게 만들었기 때문입니다.
그럼 SQLException은 어디로 갔을까?????????????
4.1.1. 초난감 예외처리
먼저 잘못된 예외 처리 습관을 살펴보면,
예외를 잡고 아무것도 하지 않는 코드
이 코드는 컴파일 오류를 없애는 데는 성공하지만, 예외가 발생했다는 사실 자체를 완전히 무시합니다.
DB 접속 실패
SQL 문법 오류
네트워크 단절
커넥션 풀 고갈 등
이 모든 상황이 정상 흐름처럼 처리됩니다.
발생 가능한 문제
DB 작업이 실패해도 호출자는 성공으로 인식
데이터 정합성 붕괴
이후 로직에서 원인 추적 불가
특히 트랜잭션 경계 안에서 이런 코드가 존재하면 부분 실패 + 전체 성공이라는 최악의 상태를 만듭니다.
설계 관점에서의 문제
이 코드는 단순한 실수가 아니라 책임을 포기한 설계입니다.
예외를 처리하지 않음
그렇다고 전달하지도 않음
시스템이 스스로 망가질 시간을 벌어주는 코드
콘솔 출력만 하는 예외 처리
또는
개발 중에는 눈에 띌 수 있지만, 운영 환경에서는 로그를 보지 않으면 아무도 모르는 코드가 됩니다.
이 방식은 예외를 처리하는 것이 아니라 사고를 기록만 하고 방치하는 것에 가깝습니다. 운영 환경에서는 아무도 모름
로그 레벨 관리 불가
모니터링 시스템과 연동 불가
장애 발생 후 원인 추적 불가
시스템을 종료해버리는 코드
이 방식은 최악의 초난감 예외 처리입니다.
라이브러리 코드에서 JVM 종료
서버 전체 다운
호출자는 대응할 기회조차 없음
"예외를 잡는다는 것은 문제를 해결하거나, 책임 있는 곳으로 넘긴다는 것"
무의미하고 무책임한 throws
또 다른 흔한 실수는 아무 생각 없이 throws Exception을 붙이는 것입니다.
이 방식의 문제점은 명확합니다.
어떤 예외가 발생할 수 있는지 알 수 없음
호출자는 아무 정보도 얻지 못함
복구 가능한 예외마저 모두 포기
계층 설계 붕괴
모든 계층이 throws Exception을 사용하면
DAO → Service → Controller
모든 계층이 기술 예외에 오염됨
비즈니스 코드가 인프라 세부사항을 알게 됨
결과적으로 예외는 의미를 잃고, 코드는 책임을 회피하게 됩니다.
4.1.2. 예외의 종류와 특징
자바의 예외는 크게 세 가지로 나뉩니다.
Error
OutOfMemoryErrorStackOverflowErrorThreadDeath
JVM 레벨의 심각한 문제로, 애플리케이션 코드에서 처리 대상이 아닙니다.
Exception (체크 예외)
IOExceptionSQLException
반드시 catch 하거나 throws 해야 합니다.
문제는 복구가 불가능한 상황에서도 강제로 처리를 요구한다는 점입니다.
RuntimeException (언체크 예외)
NullPointerExceptionIllegalArgumentExceptionIllegalStateException
프로그래밍 오류나 논리적 결함을 나타내며, 명시적인 throws 없이도 전파됩니다.
체크 예외의 한계
초기 자바 설계는 모든 예외를 체크 예외로 강제하려는 방향이었습니다. 하지만 현실에서는 문제가 드러났습니다.
대부분의 SQLException은 코드 레벨에서 복구 불가
결국 무의미한 catch / throws만 증가
코드 가독성과 책임 구조 붕괴
이로 인해 최근의 자바 API와 프레임워크는 체크 예외를 줄이고 언체크 예외를 선호하는 방향으로 발전했습니다.
4.1.3.
예외를 다루는 방법은 크게 세 가지입니다.
예외 복구
예외가 발생해도 정상 상태로 되돌릴 수 있는 경우입니다.
네트워크 일시 장애
재시도 가능한 외부 시스템 오류
예외 회피
자신이 처리하지 않고 의미 있는 계층으로 전달합니다.
계층을 기술적으로 나누면
일반적인 Spring 애플리케이션에서 계층은 다음처럼 나뉩니다.
“의미 있는 계층”이란, 예외를 보고 판단하거나 행동을 바꿀 수 있는 계층 입니다.
각 계층이 알 수 있는 것 / 알면 안 되는 것이 다릅니다.
Controller - 사용자 요청 / DB 벤터, SQL 에러코드
Service - 비즈니스 규칙 / JDBC Api, SQLException
DAO - SQL, DB 제약조건 / 사용자 UX 정책
의미 없는 예외 회피 (잘못된 예)
DAO에서 SQLException을 그대로 던지는 경우
왜 의미가 없는가
Service 계층은
SQLException을 보고이게 중복 키인지
네트워크 오류인지
문법 오류인지 알 수 없습니다.
결국 Service는 이렇게 됩니다.
DAO에서 던지나 Service에서 던지나 차이가 없습니다.
의미 있는 예외 회피 – 예시 1 (DAO → Service)
DAO에서 예외를 전환해서 던짐
여기서 “의미”는 무엇인가
DuplicateUserIdException비즈니스적으로 해석 가능
Service 계층에서 분기 가능
Service 계층에서의 처리
이때 Service는 DB를 몰라도 됩니다. 이게 “의미 있는 계층으로 전달”입니다.
의미 있는 예외 회피 – 예시 2 (Service → Controller)
Service에서 예외를 그대로 처리하지 않는 경우
Controller에서 처리
Service는 HTTP를 모르고 Controller는 비즈니스 예외를 자기 역할에 맞게 해석합니다.
그래서 예외 회피는 언제 쓰는가
올바른 예외 회피
아무 의미 없이 던지는 것은 회피가 아니라 방기입니다.
4.1.4. 예외처리 전략
스프링은 다음과 같은 원칙을 따릅니다.
DAO 계층에서는 SQLException을 그대로 던지지 않는다
모든 JDBC 예외를 DataAccessException (RuntimeException) 으로 변환
호출자는 필요한 경우만 catch
JdbcTemplate의 메서드 시그니처는 이를 보여줍니다.
throws SQLException이 없습니다.
4.1.5. SQLException은 어떻게 됐나?
SQLException은 사라진 것이 아니라, DataAccessException으로 전환되었다.
복구 불가능한 예외를 억지로 처리하지 않도록
DAO 계층의 책임을 명확히 하고
서비스 계층에서 의미 있는 예외만 다루도록
JdbcTemplate은 SQLException을 숨긴 것이 아니라, 잘못 사용되지 않도록 구조적으로 제거한 것입니다.
Throw가 자바/JVM에서 동작하는 방식
1. throw의 정체
throw의 정체이 한 줄은 문법적으로는 간단하지만, 의미적으로는 제어 흐름을 즉시 중단시키는 명령입니다.
기술적으로 throw는 다음을 의미합니다.
“이 메서드는 더 이상 정상 경로로 실행되지 않는다”
2. JVM 관점에서 throw 실행 순서
throw 실행 순서1️⃣ 예외 객체는 이미 존재해야 합니다
이 코드는 내부적으로 다음 두 단계입니다.
throw는 객체를 생성하지 않습니다반드시
Throwable의 서브클래스 인스턴스를 받아야 합니다
2️⃣ 현재 스택 프레임 즉시 종료
예외가 던져지는 순간:
현재 메서드는 즉시 종료
return 값이 있어도 무시됨
이후 코드는 절대 실행되지 않음
출력 결과:
3️⃣ JVM은 스택을 “되감기(Unwinding)” 시작
호출 스택이 아래와 같다고 가정하겠습니다.
dao()에서 throw 발생 시:
dao()스택 프레임 제거service()에서 catch 가능한지 검사없으면
service()프레임 제거controller()로 이동없으면 스레드 종료
👉 이것을 stack unwinding이라고 합니다.
3. catch는 어떻게 선택되는가
catch 탐색 규칙 (엄격함)
위에서 아래로 검사
첫 번째로 타입이 맞는 catch만 실행
이후 catch는 무시
타입 매칭 규칙
이 예외는 다음 catch에 잡힐 수 있습니다.
IllegalStateExceptionRuntimeExceptionExceptionThrowable
하지만 다음에는 절대 잡히지 않습니다.
IOExceptionSQLException
👉 상속 관계로만 판단합니다.
4. catch에서 다시 throw하면?
throw하면?이때 JVM 관점에서는:
예외 객체는 동일
스택 언와인딩은 계속 진행
현재 메서드는 종료
즉,
catch + throw는 “여기서는 아무 것도 안 하고 통과시킨다”와 동일합니다.
5. 스택 트레이스는 언제 찍히는가
중요한 오해 하나
throw할 때 스택 트레이스가 다시 만들어진다 ❌
❌ 아닙니다.
스택 트레이스는 예외 객체가 생성될 때 캡처됩니다.
스택 트레이스는
new RuntimeException()시점 기준
그래서 이런 차이가 생깁니다
새 예외 객체 생성
스택 트레이스 새로 캡처
원인은
cause로 연결
👉 이것이 예외 전환이 의미를 가지는 이유입니다.
6. throw와 throws의 관계 (정확한 역할)
throw와 throws의 관계 (정확한 역할)throw
지금 이 순간 예외를 발생시킴
throws
이 메서드가 어떤 예외를 던질 수 있는지 계약 선언
throws는 실행과 무관throw만이 흐름을 바꿈
7. 트랜잭션과 throw
throw스프링에서 매우 중요한 지점입니다.
RuntimeException throw → 자동 롤백
Checked Exception throw → 기본적으로 롤백 안 됨
이 차이는 throw 자체가 아니라 예외 타입 때문입니다.
8. 정리: throw의 본질
throw의 본질기술적으로 throw는 다음을 동시에 수행합니다.
현재 메서드 즉시 종료
정상 제어 흐름 파괴
호출 스택을 따라 위로 전파
첫 번째 매칭되는 catch 탐색
없으면 스레드 종료
4.2. 예외 전환
예외 전환은 발생한 예외를 그대로 던지는 것이 아니라, 다른 타입의 예외로 바꿔 던지는 행위를 말합니다. 스프링의 예외전환은 단순한 포장이 아닌 아래 문제들을 해결하기 위함입니다.
JDBC의 SQLException 하나로는 원인 분기 어려움
DB마다 에러 코드 체계가 다름
DAO 인터페이스에 기술 종속 예외가 새어 나오는 문제
이를 해결하기 위해 스프링은 SQLException -> DataAccessException 전환 구조를 제공
4.2.1. JDBC의 한계
JDBC 예외 구조의 기술적 문제
JDBC에서 발생하는 모든 DB관련 예외는 단 하나의 타입 -> java.sql.SQLException. 이 예외에 모든 정보가 포함됩니다.
SQL 문법 오류
제약 조건 위반
커넥션 실패
네트워크 오류
DB 서버 다운
이걸로는 원인을 구분하기 어려움.
SQLException 내부에 들어 있는 정보
문제는 이 정보들이 신뢰하기 어렵고, DB마다 다르다는 점입니다.
비표준 SQL과 DB 종속성
errorCode의 문제
MySQL: 1062
Oracle: 1
PostgreSQL: 23505
DB가 바뀌면 코드가 깨집니다.
SQLState의 한계
SQLState는 표준이지만 현실적으로 문제가 있습니다.
DB 벤더가 정확히 지키지 않는 경우 존재
세부 원인을 표현하기에는 너무 거칠음
실제 현장에서는 거의 사용되지 않음
4.2.2. DB 에러 코드 매핑을 통한 전환
스프링의 접근 방식
스프링은 다음 전략을 사용합니다.
DB 벤더별 에러 코드 테이블을 준비
SQLException발생 시 에러 코드 확인의미 있는 DataAccessException 서브클래스로 변환
실제 매핑 구조
이 매핑 정보는 스프링 내부에 정의되어 있습니다.
DB가 바뀌어도 동일한 예외 타입이 던져집니다.
JdbcTemplate에서 실제로 벌어지는 일
JdbcTemplate은 SQLException을 그대로 밖으로 내보내지 않습니다.
4.2.3. DataAccessException 계층 구조
DataAccessException의 정체
기술적으로 DataAccessException은 다음 성격을 가집니다.
RuntimeException (unchecked)
JDBC 전용 ❌
모든 데이터 접근 기술 공통 예외 베이스
JDBC
JPA
Hibernate
JDO
MyBatis
즉, “DB에 접근하다 실패했다”는 추상적인 실패 의미를 표현하는 타입입니다.
왜 단일 클래스가 아니라 계층 구조인가
DataAccessException 하나만 있으면 이런 코드밖에 못 씁니다.
그래서 스프링은 실패 원인 기준으로 서브클래스를 나눕니다.
대표적인 하위 예외들과 의미
1️⃣ BadSqlGrammarException
SQL 문법 오류
테이블/컬럼 이름 오류
잘못된 쿼리 구조
→ 테이블 usr 없음
→ BadSqlGrammarException
👉 개발자 실수 👉 재시도 의미 없음
2️⃣ DataIntegrityViolationException
NOT NULL 위반
FK 위반
UNIQUE 제약 위반
→ DuplicateKeyException
👉 데이터 무결성 위반 👉 비즈니스 분기 가능
3️⃣ DataAccessResourceFailureException
DB 서버 다운
커넥션 풀 고갈
네트워크 장애
→ DB 연결 실패
→ DataAccessResourceFailureException
👉 시스템 장애 👉 재시도 또는 장애 전파 대상
4️⃣ OptimisticLockingFailureException
낙관적 락 실패
동시성 충돌
→ 수정된 행 수 = 0
→ OptimisticLockingFailureException
👉 동시성 제어 실패 👉 재시도 전략 가능
이 예외들은 DB 종류와 무관하게 동일한 타입으로 던져집니다.
MySQL
Oracle
PostgreSQL
H2
👉 DAO를 바꿔도 Service 코드는 유지됩니다.
4.2.4. DAO 인터페이스와 예외
기술 종속 인터페이스의 문제
이 인터페이스는 JDBC에 종속됩니다.
JPA로 변경 불가
Hibernate로 변경 불가
테스트 코드도 SQLException을 알아야 함
인터페이스가 구현 기술을 노출
DataAccessException을 사용하면
throws 선언 없음
구현체 내부에서 DataAccessException 발생
호출자는 필요할 때만 catch
DAO 인터페이스가 기술로부터 독립
4.2.5. 애플리케이션 예외와 기술 예외의 분리
기술 예외
SQLException
DataAccessException
HibernateException
공통 특징:
원인: 시스템 / 인프라
복구 불가
사용자에게 그대로 노출 되지 않음
애플리케이션 예외 (Business Exception)
공통 특징:
비즈니스 의미 있음
사용자 메시지로 변환 가능
분기 처리 대상
기술 예외 → 애플리케이션 예외 전환 예시
Last updated