개요
API 테스트하는데 문제가 생겨서 로그를 찾아봤더니 다음과 같은 에러가 발생하는 것을 확인할 수 있었다. 로그는 친절하게 트랜잭션이 걸려야 하는 상황에 트랜잭션이 걸리지 않아서 예외가 발생했음을 알려주고 있다.
org.springframework.dao.InvalidDataAccessApiUsageException: javax.persistence.Query.executeUpdate requires active transaction; nested exception is javax.persistence.TransactionRequiredException: javax.persistence.Query.executeUpdate requires active transaction
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:403)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:235)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:551)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708)
...
현재 우리 팀이 사용중인 DBMS는 Oracle과 MSSQL(레거시) 크게 두 개로 나눠지는데, 이 에러는 스프링 데이터 JPA를 사용하지 않는 MSSQL쪽에서 발생했다. 회사 정책상 MSSQL은 JPA를 쓰더라도 SP(Stored Procedure)를 써서 다음과 같이 쿼리를 날려야 한다.
return entityManager.createNamedStoredProcedureQuery(SP_NAME)
.setParameter(1, param1)
.setParameter(2, param2)
...
.executeUpdate() // 또는 execute
해결
친절한 로그가 알려준 그대로 트랜잭션을 적용하면 된다. 나는 레포지토리 메서드에 @Transactional 어노테이션을 붙여 트랜잭션을 적용하는 것으로 간단하게 해결했다.
@Repository
class MyRepositoryImpl(...) : MyRepository {
@Transactional(value = MsSqlConfigurer.TX_MANAGER_NAME)
override fun save(name: String, age: Int, birthDate: Instant): Int {
return entityManager.createNamedStoredProcedureQuery(SAVE_SOME_TABLE)
.setParameter(1, name)
.setParameter(2, age)
.setParameter(3, birthDate)
.executeUpdate()
}
...
}
executeUpdate()의 경우 트랜잭션이 걸려있는 경우에만 DML을 실행하는 메서드이다. 이와 관련해선 다음 포스트에서 좀 더 자세히 설명할 예정이다.