Skip to content

Latest commit

 

History

History
120 lines (94 loc) · 7.93 KB

File metadata and controls

120 lines (94 loc) · 7.93 KB

06 — 예외 변환(translation)

무엇을 / 왜

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 문자열의 앞부분(클래스 코드)으로 대략 분류하는 최종 폴백.

동작 흐름: 3중 폴백 체인

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)이므로 정밀 → 표준 → 대략 → 미분류 순으로 점점 느슨해진다.

핵심 메서드

SQLExceptionSubclassTranslator.doTranslate — 표준 기반 분류

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()으로 실제 원인을 꺼내 분석한다.

SQLErrorCodeSQLExceptionTranslator.doTranslate — 벤더 코드 매칭

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(벤더별 코드 묶음)는 SQLErrorCodesFactorysql-error-codes.xml을 읽어 만들고, DB 제품명으로 매칭한다. 코드 배열은 정렬돼 있어 binarySearch로 조회한다. 사용자 정의 매핑(CustomSQLErrorCodesTranslation)과 커스텀 번역기(CustomSQLExceptionTranslatorRegistry)를 먼저 시도해 확장 여지를 둔다.

설계 포인트 / 확장점

  • 책임 연쇄(Chain of Responsibility): 각 번역기는 자기가 모르면 null을 반환해 다음으로 넘긴다. 정밀한 것부터 시도하고 점점 일반화한다.
  • 3단계 커스터마이즈 훅: (1) setCustomTranslator로 전체에 우선하는 사용자 번역기, (2) sql-error-codes.xml로 벤더 코드 그룹 재정의, (3) 서브클래싱 후 customTranslate 오버라이드.
  • 절대 체크 예외를 새지 않음: 모든 경로가 실패해도 JdbcTemplateUncategorizedSQLException으로 감싸므로 호출자는 항상 비체크 DataAccessException만 본다.
  • 공유 의미 체계: 결과 예외가 spring-txorg.springframework.dao 계층이라, JPA/Hibernate(spring-orm)와 동일한 예외로 수렴한다. 데이터 액세스 기술을 바꿔도 catch (DuplicateKeyException ...)는 그대로 유효하다.

정리

예외 번역은 spring-jdbc가 단순한 JDBC 래퍼를 넘어서는 지점이다. SQLExceptionTranslator 계층은 벤더 에러코드 → JDBC4 서브클래스 → SQLState 순의 책임 연쇄로, 정밀하지만 종속적인 방식부터 표준적이지만 느슨한 방식까지 차례로 시도한다. 그 출력은 체크 예외가 아니라 기술 중립적인 DataAccessException 계층이며, 어느 단계도 분류하지 못하면 UncategorizedSQLException이라는 안전망으로 떨어진다. 다음 챕터는 생성 키, LOB, 시퀀스, 임베디드 DB 등 나머지 부가 인프라를 모아 본다.