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
view raw tt.java hosted with ❤ by GitHub

위 로그를 살펴보면 총 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)
}
view raw asd.java hosted with ❤ by GitHub

@TransactionalTransactionActions 클래스의 doExecute 메서드에 선언되어 있다.

구조를 보면 알 수 있듯 템플릿 메서드 패턴으로 되어있는데

  • 추상 클래스인 AbstractActionexecute가 외부로부터 호출된다.
  • execute는 구현체의 doExecute를 호출한다.


문제가 되는 곳은 추상 클래스의 메서드가 구현체의 메서드를 호출하는 부분이다.

이는 선언적 트랜잭션의 프록시 기반 Spring AOP 와 관련 이 있는데, 최대한 간단하게 이야기 하자면

스프링은 @Transactional 경계가 설정된 메서드가 외부로부터 호출될 때 target object가 곧바로 호출되지 않고 proxy가 끼어들어 동작 한다.

그리고 트랜잭션과 관련된 기능을 수행하면서 target object의 메서드를 호출한다.


근데 이 proxy가 가로채지 못하는 경우가 생기는데, 주로 내부 메서드에서 @Transactional 선언 메서드 호출이 일어날 때 이다.

즉, 메서드가 클래스 내부에서 호출되면 proxy가 적용되지 않고 결과적으로 @Trnasactional 적용이 되지 않는다.


TransactionsActions 클래스에 적용된 템플릿 메서드 패턴자기 자신의 (@Transactional이 적용된)메서드를 직접 호출하는 꼴이 되어 트랜잭션 적용이 되지 않은 것이다.


이를 해결하기 위한 방법으로 대략 2가지 방법을 제시하는데

  • 외부에서 호출하도록 분리
  • 내부에 멤버로 동일 타입 인스턴스 주입 및 해당 인스턴스로부터 트랜잭션 메서드 호출


위 방법 중 외부에서 호출하도록 분리하는 방법을 택했다.

그 이유는 그나마 스프링에 덜 의존적..이라고 판단을 했다.