TroubleShooting - 템플릿 메서드와 트랜잭션 미수행 현상
TroubleShooting
TransactionActions
테스트 진행 중 일종의 버그 발생
@Transactional
선언을 했음에도 트랜잭션 반영이 안되는 현상- 기대하는 바는
@Transactional
메서드 실행 시begin
, 종료 시commit or rollback
- 하지만 각 쿼리 실행 시
begin
,commit
이 수행됨(debug level 로그 확인) - 마치
@Transactional
선언이 안된 것 처럼 수행됨.
o.h.e.t.internal.TransactionImpl : begin | |
o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@7893c715] | |
o.hibernate.internal.util.EntityPrinter : com.kcb.rhea.feature.business.dbaccesstest.domain.Product{prodName=PRODUCT_NAME1, prodPrice=10000, prodId=PRODUCT_1} | |
org.hibernate.SQL : insert into PRODUCTS (PROD_NAME, PROD_PRICE, PROD_ID) values (?, ?, ?) | |
D_ID) values (?, ?, ?) | |
o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit | |
o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(2085428347<open>)] | |
o.h.e.t.internal.TransactionImpl : committing | |
o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(2085428347<open>)] after transaction | |
. | |
. | |
. | |
o.h.e.t.internal.TransactionImpl : begin | |
o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@5c8ab9de] | |
o.hibernate.internal.util.EntityPrinter : com.kcb.rhea.feature.business.dbaccesstest.domain.Member{memberAge=31, memberName=LEE, memberId=MEMBER_1} | |
org.hibernate.SQL : insert into MEMBERS (MEMBER_AGE, MEMBER_NAME, MEMBER_ID) values (?, ?, ?) | |
MBER_ID) values (?, ?, ?) | |
o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit | |
o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(790598933<open>)] | |
o.h.e.t.internal.TransactionImpl : committing | |
o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(790598933<open>)] after transaction | |
. | |
. | |
. | |
o.h.e.t.internal.TransactionImpl : begin | |
o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@844e66d] | |
o.hibernate.internal.util.EntityPrinter : com.kcb.rhea.feature.business.dbaccesstest.domain.Student{studentId=STUDENT_1, studentName=BONG} | |
org.hibernate.SQL : insert into STUDENTS (STUDENT_NAME, STUDENT_ID) values (?, ?) | |
values (?, ?) | |
o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit | |
o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(2068126708<open>)] | |
o.h.e.t.internal.TransactionImpl : committing | |
o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(2068126708<open>)] after transaction |
위 로그를 살펴보면 총 3 번의 query 실행에 따라 각각 begin
, commit
을 수행하는 것을 확인할 수 있다.
기대하는 바는 3번의 query가 하나의 트랜잭션으로 묶이고, 트랜잭션 시작 시 begin
, 세 번의 쿼리 모두 수행 후 commit
을 기대했는데 결과는 그렇지 않다.
원인 - proxy와 템플릿 메서드 패턴
@Transactional
이 적용된 구조와 코드를 살펴보면 아래와 같다.
// Action interface | |
interface Action { | |
val name: String | |
val setRoot: Boolean | |
fun execute(request: Parameter<String, Any?>): Any? | |
} | |
// AbstractAction abstract class | |
abstract class AbstractAction( ... ) : Action { | |
... | |
abstract fun doExecute(request: Parameter<String, Any?>): Any? | |
override fun execute(request: Parameter<String, Any?>): Any? { | |
... | |
return doExecute(request) | |
} | |
} | |
// TransactionActions class | |
open class TransactionActions(...) : AbstractAction(name, setRoot) { | |
private val sequentialActionExecutor = SequentialActionExecutor(*actions, abortOnException = true) | |
@Transactional(rollbackFor = [Exception::class) | |
override fun doExecute(request: Parameter<String, Any?>): Any = sequentialActionExecutor.invoke(request) | |
} |
@Transactional
은 TransactionActions
클래스의 doExecute
메서드에 선언되어 있다.
구조를 보면 알 수 있듯 템플릿 메서드 패턴으로 되어있는데
- 추상 클래스인
AbstractAction
의execute
가 외부로부터 호출된다. execute
는 구현체의doExecute
를 호출한다.
문제가 되는 곳은 추상 클래스의 메서드가 구현체의 메서드를 호출하는 부분이다.
이는 선언적 트랜잭션의 프록시 기반 Spring AOP
와 관련 이 있는데, 최대한 간단하게 이야기 하자면
스프링은 @Transactional
경계가 설정된 메서드가 외부로부터 호출될 때 target object
가 곧바로 호출되지 않고 proxy
가 끼어들어 동작 한다.
그리고 트랜잭션과 관련된 기능을 수행하면서 target object
의 메서드를 호출한다.
근데 이 proxy
가 가로채지 못하는 경우가 생기는데, 주로 내부 메서드에서 @Transactional
선언 메서드 호출이 일어날 때 이다.
즉, 메서드가 클래스 내부에서 호출되면 proxy
가 적용되지 않고 결과적으로 @Trnasactional
적용이 되지 않는다.
⇒ TransactionsActions
클래스에 적용된 템플릿 메서드 패턴
은 자기 자신의 (@Transactional이 적용된
)메서드를 직접 호출하는 꼴이 되어 트랜잭션 적용이 되지 않은 것이다.
이를 해결하기 위한 방법으로 대략 2가지 방법을 제시하는데
- 외부에서 호출하도록 분리
- 내부에 멤버로 동일 타입 인스턴스 주입 및 해당 인스턴스로부터 트랜잭션 메서드 호출
위 방법 중 외부에서 호출하도록 분리하는 방법을 택했다.
그 이유는 그나마 스프링에 덜 의존적..이라고 판단을 했다.