이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Spring은 SQL 예외와 JPA 예외를 왜 DataAccessException 계층으로 변환하는가?
  • Repository에서 데이터 접근 예외를 어디까지 잡고 어디까지 흘려보내야 하는가?
  • 데이터베이스 제약 위반을 도메인 실패로 바꿀 때 어떤 기준을 가져야 하는가?

개요

데이터 접근 기술은 각자 다른 예외를 던진다. JDBC는 SQLException, JPA는 PersistenceException 계열을 사용한다. Spring은 이런 기술별 예외를 DataAccessException이라는 unchecked 예외 계층으로 변환해 애플리케이션 코드가 특정 기술에 덜 묶이게 한다. 변환된 예외는 원본 예외를 감싸므로, 기술 정보를 모두 잃는 것이 아니라 상위 계층의 의존성을 줄이는 것이다.

예외 변환은 Repository 추상화의 중요한 부분이다. 데이터베이스가 어떤 오류를 냈는지를 그대로 상위 계층에 노출하는 것이 아니라, 애플리케이션이 이해할 수 있는 실패로 다루게 한다.

예외 변환 원리

JdbcTemplate은 JDBC 예외를 자동으로 DataAccessException 계층으로 변환한다. @Repository가 붙은 Bean은 PersistenceExceptionTranslationPostProcessor 같은 후처리기의 대상이 될 수 있어 JPA 같은 persistence 예외도 Spring의 데이터 접근 예외로 변환될 수 있다.

DataAccessException은 checked exception이 아니다. 대부분의 데이터 접근 실패는 호출 지점에서 즉시 복구하기 어렵기 때문이다. 대신 상위 예외 처리 계층에서 로그를 남기고 사용자에게 안정적인 오류 응답을 제공하는 방식이 자연스럽다.

잡을 예외와 흘려보낼 예외

Repository에서 모든 데이터 접근 예외를 잡아 null이나 빈 리스트로 바꾸는 것은 위험하다. 시스템 장애와 정상적인 “데이터 없음”이 구분되지 않기 때문이다.

데이터 없음은 Optional.empty()나 빈 컬렉션으로 표현할 수 있다. 반면 DB 연결 실패, SQL 문법 오류, 제약 위반은 실제 실패다. 이 실패는 상위 계층에서 의미 있는 도메인 예외로 바꾸거나 공통 예외 처리로 보내야 한다.

public Member getMember(Long memberId) {
    return memberRepository.findById(memberId)
            .orElseThrow(() -> new MemberNotFoundException(memberId));
}

“회원이 없다”는 Repository 기술 예외가 아니라 유스케이스의 실패다. 그래서 Service에서 도메인 의미를 가진 예외로 바꾸는 것이 자연스럽다.

제약 위반 처리

unique 제약 위반은 자주 도메인 실패와 만난다. 예를 들어 이메일 unique 제약이 실패하면 “이미 사용 중인 이메일”이라는 비즈니스 실패로 바꿀 수 있다.

다만 DB 제약 위반 메시지에 직접 의존하는 것은 취약하다. 데이터베이스마다 메시지 형식이 다르고, 제약 이름이 바뀔 수 있다. 가능하면 사전 조회와 DB unique 제약을 함께 두고, 최종 방어선으로 제약 위반을 잡아 안정적인 오류 코드로 변환한다. 제약 이름은 운영 로그와 매핑 코드에서 추적 가능하게 관리하되, 사용자 응답에는 내부 제약 이름을 그대로 노출하지 않는다.

실전 팁

  • 데이터 없음과 데이터 접근 실패를 구분한다.
  • Repository는 기술 예외를 숨기되 실패를 삼키지 않는다.
  • unique 제약은 애플리케이션 검증과 DB 제약을 함께 둔다.
  • DB 제약 이름은 운영 로그와 예외 매핑에서 추적 가능하게 관리한다.
  • 예외 변환을 기대하는 Repository에는 @Repository와 예외 변환 후처리기가 실제로 적용되는지 확인한다.
  • 데이터 접근 예외를 API 응답으로 직접 노출하지 말고 도메인 또는 공통 오류 코드로 변환한다.

위험 신호!

  • catch (DataAccessException e) 후 빈 리스트를 반환한다.
  • SQL 예외 메시지를 그대로 사용자에게 보여 준다.
  • unique 제약 없이 사전 조회만으로 중복을 막으려 한다.
  • Repository가 HTTP 상태 코드까지 알고 있다.
  • @Repository 없이 persistence 예외가 변환될 것이라고 기대한다.
  • DB 제약 위반을 모두 500으로만 처리해 사용자가 수정할 수 있는 오류도 시스템 장애처럼 보인다.

확인 질문

확인 질문

  • Spring이 DataAccessException으로 예외를 변환하는 이유는 무엇인가?
    • 데이터 접근 기술별 예외 차이를 줄이고 애플리케이션이 기술 독립적인 실패 계층을 다루게 하기 위해서다.
  • 데이터 없음과 데이터 접근 실패는 어떻게 다르게 표현해야 하는가?
    • 데이터 없음은 Optional이나 빈 컬렉션, 유스케이스 예외로 표현하고, DB 장애나 제약 위반은 실패로 처리해야 한다.
  • unique 제약 위반을 다룰 때 조심할 점은 무엇인가?
    • DB 메시지 문자열에 과하게 의존하지 말고 안정적인 도메인 오류 코드로 변환해야 한다.

참고 문서