[Spring] @Transactional: 트랜잭션 전파 처리과정
스프링은 어떻게 트랜잭션 전파를 처리하는지 알아보자
📌 서론
스프링 프레임워크에서 @Transactional 애노테이션을 사용할 때, 기본적인 전파 수준(propagation level)은 Propagation.REQUIRED다.
이 전파 수준은 현재 진행 중인 트랜잭션이 있으면 해당 트랜잭션에 참여하고, 없으면 새로운 트랜잭션을 시작하는 것이다. 즉, 메서드가 트랜잭션 내에서 실행되도록 보장해 준다.
그런데 이렇게만 알고 넘기기엔 답답한 마음이 들었다. 스프링은 내부에서 어떻게 이 전파 수준을 추출하고 해석해서 동작시키는지 정말 궁금했고 그 내용을 공부했다. 이번 포스트를 통해 그 내용을 공유한다.
이번 포스트는 정말 헷갈리고 어려울 수 있다. 쉽게 이해하기 위해 직접 코드를 따라가 보는 것이 좋다고 생각한다.
가능하다면 Spring 프로젝트를 열고 클래스를 검색해 보면서 함께 진행해 보도록 하자
1. 쉽게 이해하는 트랜잭션
먼저 이 글을 이해하기 위해서는 트랜잭션에 대한 이해가 필수다. 예시를 통해 쉽게 알아보자.
만약 큰 쇼핑몰에 가서 여러 가지 물건을 카트에 담았다고 생각해 보자. 이제 계산대로 가서 모든 물건을 결제해야 하는 상황이다. 이 과정 전체를 하나의 '트랜잭션'으로 볼 수 있다.
1. 트랜잭션 시작
- 쇼핑몰에서 카트에 물건을 담기 시작하는 순간부터 나의 '트랜잭션'이 시작된다. 스프링에서 @Transactional 애노테이션이 붙은 메서드를 호출하는 순간, 새로운 트랜잭션이 시작되는 것과 같다.
2. 변경 사항 기록
- 내가 쇼핑하면서 카트에 담은 모든 물건들은 결제 전까지 '임시 저장' 상태에 있다. 마찬가지로, 트랜잭션 내에서 데이터베이스에 대한 변경(데이터 추가, 수정, 삭제 등)은 모두 임시로 기록되고, 트랜잭션이 성공적으로 완료될 때까지 실제 데이터베이스에 반영되지 않는다.
3. 트랜잭션 확인
- 계산대에서 모든 물건을 스캔하고, 가격을 확인한 후에 결제를 진행하는 과정이 이에 해당한다. 스프링에서는 이 단계에서 모든 비즈니스 로직이 제대로 수행되었는지 검증하게 된다.
4. 커밋 또는 롤백
- 결제가 성공적으로 이루어지면, 모든 변경 사항(카트에 담긴 물건들의 구매)이 확정되어 '커밋'되는 것이다. 이때 모든 데이터 변경 사항이 데이터베이스에 영구적으로 반영된다.
- 만약 결제 과정에서 문제가 발생하면(예: 카드 거부, 재고 부족 등), 모든 거래는 취소되고, 카트에 담긴 물건들은 원래대로 돌아가게 된다. 이를 '롤백'이라고 하며, 트랜잭션이 실패했을 때 모든 변경 사항을 취소하는 과정이다.
2. 간단하게 트랜잭션 처리 과정 이해하기 (로직의 흐름)
트랜잭션의 기본적인 원리를 이해했다면 이번에는 스프링에서 @Transactional 애노테이션이 붙은 서비스 메서드가 호출될 때 트랜잭션 처리 과정을 간단히 알아보자. 앞으로 설명할 내용이 굉장히 복잡하니 먼저 흐름을 간단히 보고 넘어가는 것이 좋다. (바로 이해하긴 어려우니 보고 넘기자)
1. 컨트롤러 호출:
- 사용자의 요청에 따라 컨트롤러가 호출되고, 컨트롤러는 비즈니스 로직을 처리하기 위해 트랜잭션이 걸린 서비스 메서드 A를 호출한다.
2. TransactionInterceptor 동작:
- 서비스 메서드 A가 @Transactional 애노테이션으로 표시되어 있으면, 스프링 AOP가 TransactionInterceptor를 사용하여 해당 메서드 호출을 가로챈다. TransactionInterceptor는 TransactionAttributeSource를 조회하여 메서드에 적용된 트랜잭션 속성을 파악한다.
3. TransactionAttribute 추출:
- TransactionAttributeSource의 구현체인 AnnotationTransactionAttributeSource는 리플렉션을 사용하여 메서드 A에 적용된 @Transactional 애노테이션을 찾아내고, 애노테이션에 명시된 트랜잭션 속성(예: 전파 방식, 격리 수준 등)을 분석한다.
4. TransactionAttribute 해석:
- 분석된 트랜잭션 속성은 TransactionAttribute 객체로 변환된다. 이 과정에서 TransactionAnnotationParser 구현체인 SpringTransactionAnnotationParser가 사용되어 애노테이션의 속성을 실제 TransactionAttribute 객체로 변환한다.
5. 트랜잭션 시작 또는 참여 결정:
- TransactionAttribute의 전파 속성에 따라 TransactionInterceptor는 현재 진행 중인 트랜잭션이 있으면 해당 트랜잭션에 참여하거나, 없으면 새로운 트랜잭션을 시작한다. 기본적으로 Propagation.REQUIRED가 사용되므로, 대부분의 경우 현재 트랜잭션이 없으면 새 트랜잭션이 시작된다.
6. 서비스 메서드 A 실행:
- 트랜잭션의 시작 또는 참여가 결정된 후, 서비스 메서드 A의 비즈니스 로직이 실행된다. 메서드 A 내에서 다른 @Transactional 메서드를 호출하면, 해당 메서드들도 동일한 과정을 거쳐 트랜잭션에 참여하게 된다.
7. 트랜잭션 커밋 또는 롤백:
- 서비스 메서드 A의 로직이 성공적으로 완료되면 트랜잭션은 커밋되어 데이터 변경 사항이 반영된다. 실행 중 예외가 발생하면 트랜잭션은 롤백되어 모든 데이터 변경 사항이 취소된다.
이 과정을 통해 스프링은 @Transactional 애노테이션이 적용된 메서드 호출 시 적절한 트랜잭션 관리를 보장하며, 전파 속성에 따라 트랜잭션의 범위와 참여 방식을 결정한다. 이제부터 상세히 알아보자
3. @Transactional의 전파 설정은 스프링 내부적으로 어떻게 추출하고 해석될까?
트랜잭션 전파 설정 이해하기
- 트랜잭션 전파 설정은 @Transactional 애노테이션을 사용할 때 메서드나 클래스 레벨에서 지정한다. 이 설정은 TransactionInterceptor 내부에서 TransactionAttributeSource 객체에 의해 추출되고 해석된다.
TransactionInterceptor 코드를 살펴보자
- TransactionInterceptor는 스프링 프레임워크의 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)를 이용해서 트랜잭션 관리를 하는 중요한 부분이다.
- TransactionInterceptor는 TransactionAttributeSource로부터 트랜잭션 속성(예: 전파 방식, 격리 수준, 읽기 전용 여부 등)을 조회하고, 이러한 속성에 따라 실제 트랜잭션을 어떻게 처리할지 결정하는 역할을 한다.
TransactionInterceptor의 작동 방식을 다이어그램으로 이해해 보자
1. 클라이언트 호출: 클라이언트가 메서드를 호출하면
2. 프록시: 스프링 AOP 프록시가 이를 가로채서 TransactionInterceptor에 전달한다.
3. TransactionInterceptor 실행: TransactionInterceptor는 TransactionManager를 사용해 필요에 따라 트랜잭션을 시작하고
4. 대상 메서드 실행: 실제 대상 메서드(비즈니스 로직)를 실행한다.
5. 트랜잭션 커밋 또는 롤백: 실행 결과에 따라 트랜잭션을 커밋하거나 롤백한다.
- 이 과정을 거치면서 TransactionInterceptor는 메서드 실행 전후로 트랜잭션 관리를 적절히 해주는 역할을 하게 된다. AOP를 이용함으로써 비즈니스 로직에서는 트랜잭션 관리에 신경 쓸 필요 없이 핵심 기능 구현에 집중할 수 있어서 코드가 훨씬 깔끔하고 유지보수하기 좋아진다.
다음으로 TransactionAttributeSource 코드를 알아보자
- TransactionAttributeSource는 메서드나 클래스에 적용된 트랜잭션 관련 메타데이터를 추출하는 역할을 한다.
- TransactionAttributeSource 코드의 구현체 중 하나인 AnnotationTransactionAttributeSource는 @Transactional 애노테이션을 분석하여 TransactionAttribute 객체를 생성한다.
- TransactionAttribute 객체는 트랜잭션의 속성 정보를 담고 있어서 트랜잭션을 어떻게 처리할지 결정하는 데 중요한 역할을 한다.
TransactionAttributeSource는 TransactionInterceptor 클래스의 invoke 메서드에서 사용된다.
- invoke 메서드 안에서 invokeWithinTransaction 메서드를 호출하면 TransactionAttributeSource가 사용되어서 메서드에 적용된 트랜잭션 속성을 조회하고, 이에 기반하여 트랜잭션을 어떻게 처리할지 결정하는 과정을 볼 수 있다.
- 그러나 이 코드만으로는 구체적인 트랜잭션 전파 설정을 확인할 수는 없으니, 트랜잭션 속성을 정의하는 부분(@Transactional 애노테이션)이나 TransactionAttributeSource의 구현을 살펴봐야 한다.
invoke 메서드에서 호출하는 invokeWithTransaction() 메서드
TransactionAspectSupport 클래스에 선언된 invokeWithTransaction 메서드에서 TransactionAttributeSource를 사용해서 트랜잭션의 메타데이터를 추출한다.
- 이 메서드는 실제로 트랜잭션이 필요한 메서드가 호출될 때 실행되며, @Transactional 애노테이션에 지정된 속성(전파 방식, 격리 수준 등)을 기반으로 트랜잭션을 관리한다.
- 이 과정에서 TransactionAttributeSource를 사용하여 메서드에 적용된 트랜잭션 속성을 조회하고, 이를 토대로 트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 등의 작업을 수행하는 것이다.
4. TransactionAttributeSource의 구현체 AnnotationTransactionAttributeSource
@Transactional의 메타데이터를 추출하는 TransactionAttributeSource의 구현체를 알아보자
- TransactionAttributeSource 인터페이스의 구현체 중에서 스프링이 트랜잭션 속성, 특히 @Transactional 애노테이션을 처리하기 위해 주로 사용하는 구현체는 AnnotationTransactionAttributeSource다.
- AnnotationTransactionAttributeSource 클래스는 메서드나 클래스 레벨에서 선언된 @Transactional 애노테이션을 리플렉션을 통해 찾아내고, 애노테이션에 명시된 트랜잭션 관련 정보(예: 전파 방식, 격리 수준, 타임아웃, 읽기 전용 여부 등)를 분석한다. 분석된 정보는 TransactionAttribute 객체로 변환되어, 트랜잭션을 어떻게 처리할지 결정하는 데 사용된다.
AnnotationTransactionAttributeSource 이해하기
- AnnotationTransactionAttributeSource는 TransactionAnnotationParser 인터페이스의 구현체들과 함께 작동한다. 이 구현체들은 @Transactional 애노테이션을 실제로 파싱 하고, 애노테이션에서 정의된 속성을 TransactionAttribute 객체로 변환하는 역할을 수행한다.
- 스프링에서는 기본적으로 TransactionAnnotationParser의 구현체 중에서 SpringTransactionAnnotationParser가 사용되지만 필요에 따라 다른 구현체들(JTA, EJB 등)도 사용될 수 있다.
TransactionAnnotationParser 인터페이스 살펴보기
- 구현체는 아래 이미지에 있는 3개가 존재한다. (Spring, Ejb3, Jta)
TransactionAnnotationParser의 구현체는 어떻게 사용될까?
- 트랜잭션의 트랜잭션 속성을 분석하기 위해서 사용하는(위에서 설명) AnnotationTransactionAttributeSource클래스의 생성자를 살펴보면 TransactionAnnotationParser 구현체들을 주입하고 있다는 것을 확인할 수 있다. (Spring, Jta, Ejb3)
TransactionAnnotationParser의 구현체들은 determineTransactionAttribute 메서드에서 사용된다.
AnnotationTransactionAttributeSource 클래스의 핵심 메서드 determineTransactionAttribute
- 이 메서드는 주어진 AnnotatedElement (클래스나 메서드)에 적용된 트랜잭션 애노테이션을 분석하고, 해당 애노테이션의 속성을 기반으로 TransactionAttribute 객체를 생성하거나 반환한다.
- 이 과정에서 TransactionAnnotationParser 구현체들을 순회하며 애노테이션을 파싱 하고, 트랜잭션 속성을 결정한다.
이 메서드 안에서 호출하는 parseTransactionAnnotation 메서드도 살펴보자
- 위에서 TransactionAnnotationParser 구현체들을 순회하며 애노테이션을 파싱 하고, 트랜잭션 속성을 결정한다.라고 설명했는데 이때 아래의 parseTransactionAnnotation 메서드를 각 구현체를 순회하면서 호출한다.
- (이때 구현체중 SpringTransactionAnnotationParser 클래스에서 호출하는 메서드가 핵심이다.)
5. TransactionAnnotationParser의 구현체 SpringTransactionAnnotationParser
SpringTransactionAnnotationParser란?
- SpringTransactionAnnotationParser는 @Transactional 애노테이션의 속성을 실제로 파싱 하는 곳이며, 여기서 전파 수준(propagation)을 포함한 다양한 트랜잭션 속성을 해석하고, TransactionAttribute 객체에 설정한다.
첫 번째 parseTransactionAnnotation 메서드 이해하기
여기서 알고 넘어가야 할 부분이 있다. SpringTransactionAnnotationParser 클래스 내부의 메서드를 보면 parseTransactionAnnotation 메서드가 세 개 있고, 모두 매개변수의 타입이 다른데 이것을 자바에서는 오버로딩이라고 한다.
오버로딩은 같은 이름의 메서드를 여러 개 가지되, 매개변수의 수나 타입을 달리해서 구현하는 것을 의미한다.
- 내부에 선언된 parseTransactionAnnotation(AnnotatedElement element) 메서드는 주어진 AnnotatedElement (클래스나 메서드)에 선언된 @Transactional 애노테이션을 분석한다.
- 이 메서드는 AnnotationAttributes를 통해 애노테이션 속성을 추출하고, 이 속성들을 parseTransactionAnnotation(AnnotationAttributes attributes) 메서드로 전달한다.
여기서 리플렉션이 사용되어 애노테이션을 분석한다. (AnnotatedElement 인터페이스 알아보기)
AnnotatedElement 인터페이스는 자바 리플렉션 API의 일부로, 클래스, 메서드, 필드 등 프로그램 요소에 선언된 애노테이션을 읽을 수 있는 방법을 제공한다. AnnotatedElement는 다음과 같은 메서드들을 포함하고 있다.
- getAnnotation(Class<T> annotationClass): 지정된 타입의 애노테이션이 해당 요소에 존재하면 그 애노테이션을 반환
- getAnnotations(): 해당 요소에 존재하는 모든 애노테이션을 반환
- getDeclaredAnnotations(): 해당 요소에 직접 선언된 모든 애노테이션을 반환
이 인터페이스를 통해 애노테이션의 메타데이터, 즉 애노테이션에 설정된 값들을 런타임에 읽을 수 있다. 이 과정에서 사용되는 것이 리플렉션이다. (사진에는 모두 담을 수 없어 2개의 메서드만 캡처했다.)
조금 더 자세히 알아보자
리플렉션은 프로그램이 자기 자신을 조사하고 수정할 수 있는 기능이다.
자바에서는 클래스나 메서드 같은 코드의 구조를 런타임에 살펴볼 수 있게 해 준다.
- AnnotatedElement는 자바 리플렉션에서 사용하는 인터페이스 중 하나로, 클래스나 메서드에 붙은 애노테이션(주석 같은 거야) 정보를 가져올 수 있게 해 준다.
- 예를 들어, @Transactional이라는 애노테이션이 서비스 클래스 내부의 한 메서드에 붙어있다고 치자. 이 애노테이션은 그 메서드를 실행할 때 특정한 트랜잭션(데이터베이스 작업을 안전하게 처리하는 방법) 규칙을 적용하라는 의미를 가지고 있다.
@Nullable
public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) {
AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(element, Transactional.class, false, false);
return attributes != null ? this.parseTransactionAnnotation(attributes) : null;
}
- parseTransactionAnnotation(AnnotatedElement element) 메서드는 특정 element (클래스나 메서드 같은)에 @Transactional 애노테이션이 붙어있는지 보고, 붙어있다면 그 애노테이션의 설정(예를 들어, 트랜잭션 전파 방식이나 격리 수준 같은 것)을 읽어서 처리한다.
- 여기서 AnnotatedElementUtils.findMergedAnnotationAttributes(element, Transactional.class, false, false); 이 코드에서 리플렉션을 사용하여 "이 메서드나 클래스에 특정 규칙을 적용해야 하나? 있으면 뭐라고 적혀있나?" 하고 질문하는 핵심 동작을 수행하는 것이다.
요약하자면, findMergedAnnotationAttributes 함수는 리플렉션을 사용해 애노테이션 정보를 조회하는 핵심 동작을 수행하고, parseTransactionAnnotation(AnnotatedElement element) 메서드 전체는 이 정보를 바탕으로 프로그램이 어떻게 동작할지 결정하는 과정을 포함하고 있다.
두 번째 parseTransactionAnnotation 메서드 이해하기
parseTransactionAnnotation(AnnotationAttributes attributes){ ... }
- 이 메서드는 AnnotationAttributes 객체에서 트랜잭션 관련 속성들을 읽어 들여 RuleBasedTransactionAttribute 객체를 생성하고 설정한다. (조금 길어서 사진은 필요한 부분만 넣었다.)
Propagation propagation = (Propagation)attributes.getEnum("propagation")
- 이 코드 라인에서는 @Transactional 애노테이션에 정의된 propagation 속성 값을 읽어와서 Propagation Enum으로 변환한다. 이때, @Transactional 애노테이션의 propagation 속성에 지정된 값(예: REQUIRED, REQUIRES_NEW, SUPPORTS 등)이 Propagation Enum의 상수 중 하나와 일치한다.
📌 전파(Propagation) 설정 Enum 변환 과정 이해하기
1. @Transactional 애노테이션을 사용할 때, 우리는 propagation, isolation 등의 속성에 대해 Enum 타입의 값을 지정한다. 예를 들어, propagation 속성에는 Propagation.REQUIRED, Propagation.SUPPORTS 등의 Propagation Enum 타입의 값들을 사용할 수 있다.
2. 스프링 프레임워크는 이러한 애노테이션 속성 값들을 처리할 때 내부적으로 AnnotationAttributes 객체를 사용한다. 이 객체는 애노테이션에 지정된 속성들의 값을 저장하고 관리한다.
3. AnnotationAttributes 객체의 getEnum 메서드는 특정 애노테이션 속성에 대한 Enum 상수 값을 가져오는 데 사용된다. 이 메서드를 호출할 때는 Enum 타입과 애노테이션 속성 이름을 인자로 제공한다.
4. getEnum 메서드는 애노테이션 속성 값으로 지정된 문자열(예: "REQUIRED", "SUPPORTS")을 받아서, 이를 해당 Enum 타입(예: Propagation)의 상수로 변환한다. 이 과정은 자바의 Enum.valueOf 메서드를 사용하여 수행될 수 있으며, 결과적으로 문자열 값에 해당하는 Enum 상수(예: Propagation.REQUIRED, Propagation.SUPPORTS)를 반환한다.
이러한 과정을 통해 @Transactional 애노테이션이 적용된 메서드나 클래스에 대한 트랜잭션 속성을 완전히 해석하고, 이를 TransactionAttribute 객체로 만들어 스프링 트랜잭션 관리자가 사용할 수 있게 한다.
어떻게 변환되는지 확실히 이해했다면 Propagation Enum 클래스를 살펴보자
- Propagation 열거형은 스프링 트랜잭션에서 트랜잭션 전파 행동을 정의한다.
- 각각의 열거 값은 트랜잭션의 전파 방식을 나타내며, 서비스 메서드가 다른 트랜잭션에 참여하는 방식을 결정한다. value 필드는 각 전파 방식을 내부적으로 나타내는 고유한 정수 값이다.
- REQUIRED (0): 현재 진행 중인 트랜잭션이 있으면 해당 트랜잭션에 참여하고, 없으면 새로운 트랜잭션을 시작한다.
- SUPPORTS (1): 현재 진행 중인 트랜잭션이 있으면 그 트랜잭션에 참여하고, 없으면 트랜잭션 없이 실행한다.
- MANDATORY (2): 반드시 현재 진행 중인 트랜잭션이 있어야 한다. 없으면 예외를 발생시킨다.
- REQUIRES_NEW (3): 항상 새로운 트랜잭션을 시작한다. 현재 진행 중인 트랜잭션이 있더라도 일시 중단시키고 새 트랜잭션을 시작한다.
- NOT_SUPPORTED (4): 트랜잭션을 지원하지 않는 환경에서 실행해야 할 때 사용한다. 현재 진행 중인 트랜잭션이 있으면 일시 중단시킨다.
- NEVER (5): 트랜잭션을 사용하지 않아야 한다. 현재 진행 중인 트랜잭션이 있으면 예외를 발생시킨다.
- NESTED (6): 현재 진행 중인 트랜잭션이 있으면 중첩된 트랜잭션을 시작한다. 중첩된 트랜잭션은 외부 트랜잭션에 롤백되지 않는 독립적인 커밋이나 롤백을 가질 수 있다.
📌 Propagation 속성에 따라 어떻게 트랜잭션이 적용될까?
나는 한 트랜잭션이 걸린 서비스 메서드 안에서 다른 트랜잭션이 걸린 서비스 메서드가 호출될 때 어떻게 트랜잭션 전파가 되는지 궁금했는데 이 궁금증에 대한 답은 다음과 같다.
"@Transactional 애노테이션에 지정된 propagation 속성에 따라 달라진다."
예를 들어, 첫 번째 메서드가 REQUIRED 전파 방식을 사용하고 있고, 이 메서드 내에서 두 번째 메서드를 호출하는데, 두 번째 메서드도 REQUIRED를 사용하면, 두 번째 메서드는 첫 번째 메서드와 같은 트랜잭션에서 실행된다.
하지만 두 번째 메서드가 REQUIRES_NEW를 사용하면, 두 번째 메서드는 자체적인 새로운 트랜잭션을 시작하고 첫 번째 메서드의 트랜잭션과는 독립적으로 실행되게 된다.
마지막으로 @Transactional 어노테이션을 보자
- 스프링에서 @Transactional을 쓸 때 특별한 설정을 하지 않았다면 기본적으로 '필요하면 트랜잭션 시작하고, 이미 있는 트랜잭션이 있으면 거기 참여해'라는 방식(REQUIRED)으로 동작한다.
6. 트랜잭션 메서드를 호출하는 상황을 가정해 보자
트랜잭션이 적용된 세 개의 서비스 메서드가 있고, 한 메서드가 다른 두 메서드를 호출한다. 모든 메서드에는 @Transactional 애노테이션이 적용되어 있고, 특별한 전파 방식을 지정하지 않았기에 기본 전파 방식은 REQUIRED다.
1. 서비스 메서드 A가 호출됨
- @Transactional이 적용되어 있으므로 스프링은 트랜잭션을 시작한다. 이때, 기본 전파 방식인 REQUIRED가 적용되므로, 현재 활성 트랜잭션이 없기 때문에 새로운 트랜잭션을 생성하게 된다.
2. 서비스 메서드 A 내에서 서비스 메서드 B를 호출
- 서비스 메서드 B 역시 @Transactional이 적용되어 있다. REQUIRED 전파 방식에 따라, 메서드 B는 현재 진행 중인 메서드 A의 트랜잭션에 참여한다. 즉, 메서드 A와 메서드 B는 같은 트랜잭션을 공유하게 되는 것이다.
3. 서비스 메서드 B 실행 완료 후, 서비스 메서드 A 내에서 서비스 메서드 C를 호출
- 서비스 메서드 C에도 @Transactional이 적용되어 있고, 여기서도 REQUIRED 전파 방식이 적용되어 있다. 따라서, 메서드 C는 이미 활성화된 메서드 A(그리고 메서드 B와 공유하는)의 트랜잭션에 참여한다. 이로써 메서드 A, B, C 모두 같은 트랜잭션 내에서 실행된다.
4. 서비스 메서드 C 실행 완료 후, 제어가 메서드 A로 돌아옴
- 메서드 A의 나머지 코드가 실행된다. 모든 코드 실행이 성공적으로 완료되면, 시작된 트랜잭션은 커밋된다. 만약 메서드 A, B, 또는 C에서 예외가 발생하여 트랜잭션 롤백 규칙에 맞는 경우, 전체 트랜잭션(즉, A, B, C에서의 모든 변경 사항)은 롤백된다.
이 과정에서 중요한 점은 모든 메서드가 REQUIRED 전파 방식을 사용할 때, 메서드들은 모두 처음 시작된 트랜잭션을 공유한다는 것이다. 따라서 하나의 메서드에서 발생한 데이터 변경 사항은 다른 메서드의 실행에도 영향을 미치고, 모든 메서드가 성공적으로 완료되어야만 최종적으로 트랜잭션이 커밋될 수 있다.
7. 마무리 (정리)
전체 트랜잭션 처리과정 이해하기
- 유저가 요청을 보냈을 때부터 응답받을 때까지의 모든 트랜잭션 흐름을 정리했다. (GPT 도움)
1. @Transactional 애노테이션이 붙은 서비스 메서드가 호출되면, 스프링 AOP의 TransactionInterceptor가 이를 가로챈다.
2. TransactionInterceptor는 TransactionAttributeSource를 통해 해당 메서드에 적용된 트랜잭션 속성을 조회한다.
3. TransactionAttributeSource의 구현체인 AnnotationTransactionAttributeSource는 메서드에서 @Transactional 애노테이션을 찾아내고, 이 애노테이션에 명시된 속성(예: 전파 방식, 격리 수준 등)을 분석한다.
4. 이렇게 추출하고 해석한 트랜잭션 속성은 TransactionAttribute 객체로 변환된다.
5. 그다음, TransactionInterceptor로 돌아가서 변환된 TransactionAttribute에 정의된 전파 속성에 따라 현재 진행 중인 트랜잭션이 있으면 해당 트랜잭션에 참여하거나, 없으면 새로운 트랜잭션을 시작한다.
이렇게 해서 트랜잭션의 시작, 참여, 종료(커밋 또는 롤백)까지의 전체 과정이 관리된다.
사이드 팀원 "평양냉면"님이랑 함께 공부하면서 얻은 지식입니다. 냉면님 블로그도 많이 들려주세요!!