스프링은 데이터 접근과 관련된 예외를 추상화
해 제공합니다.
기본적으로 우리는 어떤 데이터베이스 접근 기술을 사용하냐에 따라 받게되는 예외가 다릅니다. 가령 데이터를 찾지 못했을 때 MyBatis에서는 MB-111이라는 에러를 던졌다면 JdbcTemplate에서는 JT-123이라는 에러를 던져주는 형식일 수 있습니다. 만약 우리의 서비스가 이러한 에러를 직접 처리하고 있다면 MyBatis에서 JPA로 기술을 변경했을 때 에러처리에 대한 코드도 모두 수정해줘야 할 겁니다.
이러한 문제를 해결하기 위해 스프링은 데이터 접근과 관련된 예외를 추상화 해 우리에게 제공해주고 있습니다. MyBatis에서 던지는 MB-111번 에러, 그리고 JdbcTemplate에서 던지는 JT-123형태의 에러를 모두 DataAccessException
형태의 에러로 변환해 우리에게 전달해 줍니다.
덕분에 우리는 DataAccessException
에러를 처리하는 코드만 필요할 뿐 사용하는 기술에 종속적인 에러처리를 설계하지 않아도 됩니다.
DataAccessException
입니다. 그리고 DataAccessException
는 RuntimeException
을 상속받고 있습니다. 스프링이 제공하는 모든 데이터 접근 관련 예외는 catch하지 못했을 때 throws를 명시적으로 작성할 필요가 없다는 얘기죠.DataAccessException
은 Transient
예외와 NonTransient
예외로 나뉩니다.Transient
하위 예외는 동일한 SQL을 다시 시도했을 때 성공할 가능성이 있습니다.스프링이 제공하는 SQLExceptionTranslator
를 이용하면 개발자가 직접 특정 예외를 스프링 예외로 변환할 수도 있습니다. SQLExceptionTranslator
인터페이스는 translate
를 구현하도록 명시하고 있으며 이를 이용하면 특정 예외를 스프링 예외로 변환할 수 있습니다.
try{
// DB works
}catch (SQLException e){
SQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException springException = exTranslator.translate("작업 명", sql, e);
log.info("springException", springException);
throw springException;
}
translate()
메서드는 첫 인자로 예외에 대한 설명, 두번째 인자로 실행한 sql, 세번째 인자로 발생한 SQLException을 받아 DataAccessException을 반환합니다.스프링에서는 데이터 액세스 계층에서 @Repository
어노테이션을 사용하면 데이터베이스 예외가 아닌 DataAccessException
계열의 스프링 예외를 받게 됩니다.
@Repository
를 통해 해당 빈이 예외 변환 AOP의 적용 대상 빈으로 등록되고, 스프링 부트는 기본적으로 AOP를 적용하고 있기 때문에 가능한 일입니다.
스프링은 PersistenceExceptionTranslationPostProcessor
라는 빈 후처리기를 등록해 뒀습니다. 해당 빈 후처리기를 통해 @Repository
가 붙은 빈은 PersistenceExceptionTranslationInterceptor
라는 어드바이스가 적용됩니다.
PersistenceExceptionTranslationInterceptor
는 데이터 액세스 계층의 예외를 캐치하고, 이를 DataAccessException 계열의 예외로 변환합니다. (PersistenceExceptionTranslationInterceptor는 spring-tx
모듈에서 관리됩니다.)
PersistenceExceptionTranslationPostProcessor
는 빈 후처리기입니다.
빈 후처리기는 객체를 빈으로 등록하기 전에 어떠한 조작을 하고 싶을 때
사용하는 기술입니다. 단순한 조작뿐만 아니라 스프링 컨테이너에 등록되는 객체 자체를 바꿔치기 할 수도 있습니다.
빈 후처리기는 어떤 객체에 대해
어떤 조작을 하겠다
는 정보가 필요합니다. 이러한 정보를 어드바이저
라고 합니다. 어드바이저
는 어드바이스
+ 포인트컷
으로 구성됩니다.
어드바이스
는 '어떤 조작을 하겠다'를 결정해 줍니다. 어드바이스란 적용할 부가기능 그 자체를 의미합니다.포인트컷
은 '어떤 객체에 대해'조작할지를 결정해 줍니다. 포인트컷이란 어드바이스가 적용될 위치를 의미합니다. 한마디로 pointcut에 해당하는 빈들에 대해 advice동작을 실행하라는 의미를 담고 있습니다.
public class PersistenceExceptionTranslationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor {
private Class<? extends Annotation> repositoryAnnotationType = Repository.class;
public void setRepositoryAnnotationType(Class<? extends Annotation> repositoryAnnotationType) {
Assert.notNull(repositoryAnnotationType, "'repositoryAnnotationType' must not be null");
this.repositoryAnnotationType = repositoryAnnotationType;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
super.setBeanFactory(beanFactory);
if (!(beanFactory instanceof ListableBeanFactory)) {
throw new IllegalArgumentException(
"Cannot use PersistenceExceptionTranslator autodetection without ListableBeanFactory");
}
this.advisor = new PersistenceExceptionTranslationAdvisor(
(ListableBeanFactory) beanFactory, this.repositoryAnnotationType);
}
}
주목할 부분은 여기입니다.
this.advisor = new PersistenceExceptionTranslationAdvisor(
(ListableBeanFactory) beanFactory, this.repositoryAnnotationType);
PersistenceExceptionTranslationPostProcessor
는 PersistenceExceptionTranslationAdvisor
라는 어드바이저를 가집니다.
PersistenceExceptionTranslationAdvisor
에 의해 어떤 객체가 등록될 때 어떤 동작을 적용할지가 결정됩니다.
public class PersistenceExceptionTranslationAdvisor extends AbstractPointcutAdvisor {
private final PersistenceExceptionTranslationInterceptor advice;
private final AnnotationMatchingPointcut pointcut;
public PersistenceExceptionTranslationAdvisor(
PersistenceExceptionTranslator persistenceExceptionTranslator,
Class<? extends Annotation> repositoryAnnotationType) {
this.advice = new PersistenceExceptionTranslationInterceptor(persistenceExceptionTranslator);
this.pointcut = new AnnotationMatchingPointcut(repositoryAnnotationType, true);
}
PersistenceExceptionTranslationAdvisor(
ListableBeanFactory beanFactory, Class<? extends Annotation> repositoryAnnotationType) {
this.advice = new PersistenceExceptionTranslationInterceptor(beanFactory);
this.pointcut = new AnnotationMatchingPointcut(repositoryAnnotationType, true);
}
@Override
public Advice getAdvice() {
return this.advice;
}
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
}
PersistenceExceptionTranslationPostProcessor
는 beanFactory를 입력받는 두번째 생성자를 호출하고 있습니다.
PersistenceExceptionTranslationAdvisor(
ListableBeanFactory beanFactory, Class<? extends Annotation> repositoryAnnotationType) {
this.advice = new PersistenceExceptionTranslationInterceptor(beanFactory);
this.pointcut = new AnnotationMatchingPointcut(repositoryAnnotationType, true);
}
어드바이저가 '포인트컷에 해당하는 빈들에 대해 어드바이스를 적용하라'는 의미이기 때문에 위 코드는
AnnotationMatchingPointcut(repositoryAnnotationType, true);
에 대해 PersistenceExceptionTranslationInterceptor(beanFactory);
동작을 수행하라는 의미를 담고 있습니다.
클래스와 변수 이름을 보고 다음과 같은 동작을 예상할 수 있었습니다.
new AnnotationMatchingPointcut(repositoryAnnotationType, true);
@Override
@Nullable
public Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
}
catch (RuntimeException ex) {
// Let it throw raw if the type of the exception is on the throws clause of the method.
if (!this.alwaysTranslate && ReflectionUtils.declaresException(mi.getMethod(), ex.getClass())) {
throw ex;
}
else {
PersistenceExceptionTranslator translator = this.persistenceExceptionTranslator;
if (translator == null) {
Assert.state(this.beanFactory != null,
"Cannot use PersistenceExceptionTranslator autodetection without ListableBeanFactory");
translator = detectPersistenceExceptionTranslators(this.beanFactory);
this.persistenceExceptionTranslator = translator;
}
throw DataAccessUtils.translateIfNecessary(ex, translator);
}
}
}
mi.proceed()를 실행하다가 RuntimeException이 발생했을 때 어떤 작업을 하라
라는 의미를 담고 있습니다.throw DataAccessUtils.translateIfNecessary(ex, translator);
부분입니다. 최종적으로 RuntimeException ex를 다른 예외로 반환해 throw한다는 걸 확인할 수 있습니다. public static RuntimeException translateIfNecessary(
RuntimeException rawException, PersistenceExceptionTranslator pet) {
Assert.notNull(pet, "PersistenceExceptionTranslator must not be null");
DataAccessException dae = pet.translateExceptionIfPossible(rawException);
return (dae != null ? dae : rawException);
}
translateExceptionIfPossible
을 통해 원본 예외(rawException)을 변경할 수 있다면 DataAcessException으로 바꾸고 있다는 걸 확인할 수 있습니다.JPA 자체에서 예외 변환을 수행한다
는 내용을 보게 됐습니다. 이걸 보고 저는 JPA가 자체적으로 DataAccessException예외로 변환해 준다
는 내용으로 이해했습니다. 그래서 '@Repository를 선언하면 이미 JPA가 변환한 DataAccessException을 다시 한번 PersistenceExceptionTranslationPostProcessor를 통해 DataAccessException로 변환하는 중복 작업이 발생하는건가?'라는 의문이 생겼습니다.JPA 자체에서 예외 변환을 수행한다
는 의미는 각각의 DB마다 다른 예외를 PersistenceException예외로 변환해 준다
는 의미였습니다.PersistenceException
을 DataAccessException
로 변환해 줍니다.JPA는 내부적으로 각각의 DB마다 다른 예외를 PersistenceException으로 변환해주고, JPA의 PersistenceException은 PersistenceExceptionTranslationPostProcessor에 의해 DataAccessException으로 변환
됩니다.Pure Java + MyBatis로 작업한 프로젝트를 SpringBoot + MyBatis로 변환하는 과정에서 Dao
계층에 단순히 @Mapper
만 적용해 뒀었는데 공통 예외처리를 위해 @Repository
가 필요하다는 팀원이 설명을 듣고 해당 내용에 대해 찾아보게 되었습니다.