Spring의 큰 장점 중 하나는 JPA나 MyBatis 같은 특정 기술의 예외를 스프링 공통 예외(DataAccessException)로 추상화해준다는 것이다. 그런데 Spring Data JPA를 쓰다 보면, 분명 쿼리 문법이 틀렸는데도 기대했던 스프링 예외가 아닌 생소한 에러 로그를 마주할 때가 있다.
왜 어떤 상황에서는 예외 추상화가 적용되고, 어떤 상황에서는 날것의 에러가 터지는 걸까?
결론부터 말하면, 에러가 스프링의 프록시(Proxy)가 가로챌 수 있는 시점에 터졌느냐가 관건이다.
스프링의 예외 변환 메커니즘(PersistenceExceptionTranslator)은 @Repository가 붙은 빈의 메서드 호출을 AOP 프록시가 가로챌 때 작동한다. 즉, 프록시라는 그물망 안에서 에러가 터져야 스프링이 예쁜 예외로 포장해 줄 수 있다는 뜻이다.
Spring Data JPA는 서비스 운영 중 발생할 실수를 줄이기 위해 "잘못된 쿼리는 앱을 띄우기 전에 미리 잡자"는 전략을 취한다.
예를 들어 @Query에 작성한 JPQL에 오타가 있거나, 메서드 이름 기반 쿼리에서 존재하지 않는 엔티티 필드명을 사용한 경우, 스프링은 애플리케이션 로딩 단계(빈 초기화)에서 인터페이스를 하나하나 스캔하며 쿼리의 유효성을 미리 파싱하고 검증하려고 시도한다.
여기서 중요한 점은 이 에러가 터지는 '위치'다. 이 시점은 리포지토리 프록시가 생성되어 실제 메서드 메서드 호출을 가로채는 실행 단계가 아니기 때문에, 에러가 스프링의 예외 추상화 그물망(Proxy) 바깥에서 발생한 것으로 간주된다. 결과적으로 스프링의 예외 추상화가 적용되지 않고 BeanCreationException이나 IllegalArgumentException 같은 날것의 에러가 그대로 노출되며 애플리케이션 시작 자체가 실패하게 된다.
반면, 우리가 기대하는 DataAccessException 계열의 예외는 대개 런타임(Runtime)에 발생한다.
createQuery()가 실행되는 시점에 에러가 터진다. 이때는 프록시가 이미 작동 중이므로 에러를 가로채서 변환해 준다.Spring Data JPA를 쓰더라도 모든 쿼리 오류가 애플리케이션 시작 시점에 잡히는 것은 아니다. 다음과 같은 상황에서는 검증이 실제 실행 시점(Runtime)으로 밀려나기도 한다.
@Query(nativeQuery = true): JPQL과 달리 사전 검증이 충분히 이루어지지 않아, 실제 메서드 호출 시점에 SQL 오류가 발생한다.EntityManager.createQuery를 직접 사용하는 경우, 로직이 실행될 때 쿼리가 생성되므로 런타임에 에러가 발생한다.이 상황들은 모두 애플리케이션이 이미 뜬 상태에서 프록시를 통과하는 시점에 예외가 발생한다. 따라서 이때는 우리가 기대했던 대로 스프링의 예외 추상화가 적용되어 DataAccessException 계열의 예외를 마주하게 된다.
결국 핵심은 에러가 터지는 시점과 프록시(Proxy)의 경계선이다.
"왜 예외 추상화가 적용 안 되지?"라고 고민하기 전에, 에러 로그가 앱이 뜨는 중에 찍혔는지 아니면 실제 메서드를 실행할 때 찍혔는지부터 확인하자. 그 시점이 스프링이 개입할 수 있는지 없는지를 결정하는 명확한 기준이다.