스프링 트랜잭션 어노테이션 모음
스프링프레임워크를 활용해서 개발하게 되면 트랜잭션이 필요한 메서드에 간단하게 @Transactional 어노테이션을 붙여서 사용한다. 그런데 에디터에 @Transactional을 작성하려고 하면 다음 세개의 어노테이션이 자동 완성으로 제안받는다.
일단 TransactionAttribute는 평소에 사용하는 Transaction과 이름이 다르니까 패스. 나머지 두개의 동명의 @Transactional을 두고 고민에 빠진다. 결론은 생각보다 단순하게 난다.
스프링프레임워크에서 제공하는 어노테이션을 쓰는게 맞겠지
하고 가장 상단의 @o.s.t.a.Transactional를 선택하게 된다.
@org.springframework.transaction.annotation.Transactional
public void doSomething() {
doSomething();
}
그런데 문서를 읽어보면 세개 어노테이션 모두 트랜잭션 속성을 명시하는 어노테이션이라는 점이 동일하다.
jakarta.transaction.Transactional(JTA), jakarta.ejb.TransactionAttribute(EJB)을 붙이면 트랜잭션 로직이 동작하지 않을까? 결론부터 말하자면 Spirng, JTA, EJB 어노테이션 중 어떤 것을 사용해도 트랜잭션 로직은 작동한다.
지난 글 @Transactional은 어떻게 만들어졌을까?에서 TransactionAttributeSource와 구현체인 AnnotationTransactionAttributeSource에 대해서 설명했다. 전자는 트랜잭션 속성을 제공하는 인터페이스이고 후자는 그의 구현체 클래스로 어노테이션을 통해서 트랜잭션 속성을 제공(source)한다.
트랜잭션 어노테이션이 붙은 메서드가 호출되면 TransactionInterceptor이 호출을 가로채 부모 클래스의 메서드 TransactionAspectSupport.invokeWithinTransaction을 호출된다. 메서드 앞부분에서 AnnotationTransactionAttributeSource가 메서드로부터 트랜잭션 속성(TransactionAttribute)를 가져와서 TransactionManager를 생성한다.
<TransactionAspectSupport.java>
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
...(중략)
// If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource();
/*트랜잭션 속성을 가져온다*/
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
/*트랜잭션 속성 정보를 넘겨 트랜잭션 매니저를 가져온다*/
final TransactionManager tm = determineTransactionManager(txAttr);
...
}
AnnotationTransactionAttributeSource 의 getTransactionAttribute 메서드를 타고 들어가보면 annotationParsers를 순회하며 AnnotatedElement를 파싱하는 것을 확인할 수 있다. 그리고 파싱 결과물이 null이 아니면 그 결과를 반환한다.
public class AnnotationTransactionAttributeSource extends
...
private final Set<TransactionAnnotationParser> annotationParsers;
...
protected TransactionAttribute determineTransactionAttribute(AnnotatedElement element) {
for (TransactionAnnotationParser parser : this.annotationParsers) {
TransactionAttribute attr = parser.parseTransactionAnnotation(element);
if (attr != null) {
return attr;
}
}
return null;
}
...
TransactionAnnotationParser의 문서를 보면
Strategy interface for parsing known transaction annotation types. AnnotationTransactionAttributeSource delegates to such parsers for supporting specific annotation types such as Spring's own Transactional, JTA 1.2's Transactional or EJB3's TransactionAttribute.
트랜잭션 어노테이션을 파싱하는 인터페이스라고 기술돼있다. 그리고 스프링의 Transactional, JTA의 Transactional 그리고 EJB의 TransactionAttribute가 대상이 되는 어노테이션이라고 명시돼있다. 이미 문서에도 세개의 어노테이션 모두를 파싱한다고 답이 나와 있지만 코드를 보며 더블체크해보자.
실제로 this.annotationParsers에 세 어노테이션의 파서가 모두 추가돼는지부터 확인하자. 주석을 보며 코드를 읽어내려가자.
<AnnotationTransactionAttributeSource.java>
static {
/*클래스로더에 JTA와 EJB가 로드돼있는지 확인한다. */
ClassLoader classLoader = AnnotationTransactionAttributeSource.class.getClassLoader();
jta12Present = ClassUtils.isPresent("jakarta.transaction.Transactional", classLoader);
ejb3Present = ClassUtils.isPresent("jakarta.ejb.TransactionAttribute", classLoader);
}
public AnnotationTransactionAttributeSource(boolean publicMethodsOnly) {
this.publicMethodsOnly = publicMethodsOnly;
/*JTA와 EJB 트랜잭션 어노테이션이 로드 돼있다면 세개 어노테이션의 파서를 모두 추가한다*/
if (jta12Present || ejb3Present) {
this.annotationParsers = new LinkedHashSet<>(4);
this.annotationParsers.add(new SpringTransactionAnnotationParser());//Spring Annotation
if (jta12Present) {
this.annotationParsers.add(new JtaTransactionAnnotationParser()); //JTA Annotation
}
if (ejb3Present) {
this.annotationParsers.add(new Ejb3TransactionAnnotationParser());//EJB Annotation
}
}
else { /*jta와 ejb 어노테이션이 로드돼있지 않다면 스프링 트랜잭션 어노테이션 파서만 로드한다*/
this.annotationParsers = Collections.singleton(new SpringTransactionAnnotationParser());
}
}
JTA와 EJB 트랜잭션 어노테이션이 클래스로더에 올라가 있다면 Spring 어노테이션을 포함한 세개의 어노테이션 파서 모두가 AnnotationAttributeSource 클래스에 포함된다. 따라서 AnnotationAttributeSource는 세개 어노테이션으로 부터 트랜잭션 속성을 제공 받아 트랜잭션 속성(TransactionAttribute)를 제공한다. TransactionAttribute는 트랜잭션 생성하는데 활용된다.
<TransactionAspectSupport.java>
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class <?> targetClass,
final InvocationCallback invocation) throws Throwable {
...
//어노테이션으로부터 트랜잭션 속성을 가져온다.
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
/* 트랜잭션을 생성하고 시작한다.
* 단 조건부로 트랜잭션 정보가 있는 경우에만 트랜잭션을 시작한다. 이름도 ...ifNecessary이다.(참고) */
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
참고: @Transactional은 어떻게 만들어졌을까?