java.sql.SQLException은 두 가지 문제가 있다. (1) 체크 예외라서 모든 호출 지점이 try/catch 또는 throws를 강요받고, (2) 오류 종류(SQL 문법 오류/중복 키/락 경합/타임아웃)를 벤더별 정수 에러코드나 SQLState로만 구분하므로 이식성 있게 분기할 수 없다. org.springframework.jdbc.support의 예외 번역 계층은 이 SQLException을 비체크 예외 org.springframework.dao.DataAccessException의 의미 있는 하위 타입으로 바꾼다. "중복 키"는 어떤 DB든 DuplicateKeyException이다.
이 번역은 JdbcTemplate.translateException에서 호출되므로(02장), 모듈을 쓰는 모든 코드가 자동으로 혜택을 본다.
SQLExceptionTranslator (인터페이스)
DataAccessException translate(task, sql, SQLException)
▲
│
AbstractFallbackSQLExceptionTranslator (추상)
- customTranslator (사용자 오버라이드, null이면 통과)
- doTranslate(...) ← 서브클래스 구현
- fallbackTranslator (실패 시 다음 단계로)
▲
┌─────────────────┼──────────────────────────────┐
│ │ │
SQLErrorCodeSQL.. SQLExceptionSubclass.. SQLStateSQL..
(벤더 에러코드) (JDBC4 예외 서브클래스) (SQLState 앞 2자리)
fallback ─────▶ fallback ─────▶
SQLExceptionSubclass SQLStateSQL..
세 가지 구체 번역기는 정밀도와 이식성의 트레이드오프가 다르다.
SQLErrorCodeSQLExceptionTranslator: 벤더별 정수 에러코드를 본다. 가장 정밀하지만 가장 벤더 종속적.sql-error-codes.xml(패키지 내장 + 사용자 오버라이드)에서 DB별 코드 그룹을 읽는다.SQLExceptionSubclassTranslator: JDBC 4가 도입한SQLException서브클래스 계층(SQLTransientException,SQLIntegrityConstraintViolationException등)을 본다. 6.0부터 기본 번역기. 표준 기반이라 이식성이 좋다.SQLStateSQLExceptionTranslator: 표준 SQLState 문자열의 앞부분(클래스 코드)으로 대략 분류하는 최종 폴백.
AbstractFallbackSQLExceptionTranslator.translate가 공통 골격이다.
translate(task, sql, ex)
│
├─ 1. customTranslator? → 매치되면 그 결과 반환 (사용자 최우선 오버라이드)
│
├─ 2. doTranslate(...) → 서브클래스의 본 번역. 매치되면 반환
│
└─ 3. fallbackTranslator? → 위가 모두 null이면 다음 번역기로 위임
→ 그것도 null이면 최종 null
기본 구성(6.0+)에서는 이 폴백이 다음처럼 체인을 이룬다.
SQLExceptionSubclassTranslator.doTranslate
│ (JDBC4 서브클래스로 분류 시도)
│ 실패 → null
▼ fallback
SQLStateSQLExceptionTranslator.doTranslate
│ (SQLState 앞 2자리로 대략 분류)
│ 실패 → null
▼
JdbcTemplate.translateException 가 null을 받으면
→ UncategorizedSQLException 으로 감쌈 (최종 안전망)
사용자가 sql-error-codes.xml을 클래스패스에 두면 JdbcAccessor가 대신 SQLErrorCodeSQLExceptionTranslator를 기본으로 선택하고, 그것의 fallback이 다시 SQLExceptionSubclassTranslator(→ SQLStateSQLExceptionTranslator)이므로 정밀 → 표준 → 대략 → 미분류 순으로 점점 느슨해진다.
JDBC 4 예외 계층을 instanceof로 분기한다.
if (sqlEx instanceof SQLTransientException) { // 일시적 (재시도 가능)
if (sqlEx instanceof SQLTransientConnectionException) return new TransientDataAccessResourceException(...);
if (sqlEx instanceof SQLTransactionRollbackException) // 락 경합 / 데드락
return indicatesCannotAcquireLock(state) ? new CannotAcquireLockException(...)
: new PessimisticLockingFailureException(...);
if (sqlEx instanceof SQLTimeoutException) return new QueryTimeoutException(...);
}
else if (sqlEx instanceof SQLNonTransientException) { // 영속적 (재시도 무의미)
if (sqlEx instanceof SQLIntegrityConstraintViolationException)
return indicatesDuplicateKey(state, code) ? new DuplicateKeyException(...)
: new DataIntegrityViolationException(...);
if (sqlEx instanceof SQLSyntaxErrorException) return new BadSqlGrammarException(...);
...
}SQLTransientException(일시적, 재시도 가치 있음)과 SQLNonTransientException(영속적)을 먼저 가른 뒤 세부 타입으로 내려가는 구조가 깔끔하다. 무결성 제약 위반 안에서도 SQLState/에러코드로 "중복 키"인지 추가 판별한다. BatchUpdateException이면 getNextException()으로 실제 원인을 꺼내 분석한다.
String errorCode = sqlErrorCodes.isUseSqlStateForTranslation()
? sqlEx.getSQLState()
: Integer.toString(current.getErrorCode());
if (Arrays.binarySearch(sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0)
return new BadSqlGrammarException(task, sql, sqlEx);
else if (Arrays.binarySearch(sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0)
return new DuplicateKeyException(...);
else if (Arrays.binarySearch(sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0)
return new DataIntegrityViolationException(...);
// ... PermissionDenied, DataAccessResourceFailure, CannotAcquireLock,
// DeadlockLoser, CannotSerializeTransaction 등 그룹별 매칭SQLErrorCodes(벤더별 코드 묶음)는 SQLErrorCodesFactory가 sql-error-codes.xml을 읽어 만들고, DB 제품명으로 매칭한다. 코드 배열은 정렬돼 있어 binarySearch로 조회한다. 사용자 정의 매핑(CustomSQLErrorCodesTranslation)과 커스텀 번역기(CustomSQLExceptionTranslatorRegistry)를 먼저 시도해 확장 여지를 둔다.
- 책임 연쇄(Chain of Responsibility): 각 번역기는 자기가 모르면
null을 반환해 다음으로 넘긴다. 정밀한 것부터 시도하고 점점 일반화한다. - 3단계 커스터마이즈 훅: (1)
setCustomTranslator로 전체에 우선하는 사용자 번역기, (2)sql-error-codes.xml로 벤더 코드 그룹 재정의, (3) 서브클래싱 후customTranslate오버라이드. - 절대 체크 예외를 새지 않음: 모든 경로가 실패해도
JdbcTemplate이UncategorizedSQLException으로 감싸므로 호출자는 항상 비체크DataAccessException만 본다. - 공유 의미 체계: 결과 예외가
spring-tx의org.springframework.dao계층이라, JPA/Hibernate(spring-orm)와 동일한 예외로 수렴한다. 데이터 액세스 기술을 바꿔도catch (DuplicateKeyException ...)는 그대로 유효하다.
예외 번역은 spring-jdbc가 단순한 JDBC 래퍼를 넘어서는 지점이다. SQLExceptionTranslator 계층은 벤더 에러코드 → JDBC4 서브클래스 → SQLState 순의 책임 연쇄로, 정밀하지만 종속적인 방식부터 표준적이지만 느슨한 방식까지 차례로 시도한다. 그 출력은 체크 예외가 아니라 기술 중립적인 DataAccessException 계층이며, 어느 단계도 분류하지 못하면 UncategorizedSQLException이라는 안전망으로 떨어진다. 다음 챕터는 생성 키, LOB, 시퀀스, 임베디드 DB 등 나머지 부가 인프라를 모아 본다.