Transaction이란?
Spring을 사용하다 보면 자연스럽게 MVC 구조를 접하게 되고, 이 과정에서 데이터 저장소(Model)에 접근하기 위해 JDBC, JPA, Hibernate 등의 기술을 활용하게 됩니다.
이러한 기술들을 사용하여 데이터 저장소에 요청할 쿼리를 만들고 실행하기 전까지의 과정을 하나의 작업 단위라고 하는데, 이를 한 단어로 트랜잭션이라고 부릅니다.
트랜잭션은 작업 단위 안에서 발생하는 조회, 생성, 수정, 삭제 등의 여러 작업을 하나로 묶어 처리하는데요,
예를 들어, 주문이라는 작업은 다음과 같은 단계를 포함하며, 이를 하나의 트랜잭션으로 처리합니다:
- 사용자의 주문 요청
- 상품 재고 감소
- 주문 정보 저장
- 사용자에게 주문 요청 완료 알림 발송
이처럼 주문 작업은 여러 단계를 포함하는 하나의 트랜잭션(작업 단위)이라고 할 수 있습니다
Spring Transaction
Spring에서는 이러한 트랜잭션 관리를 어노테이션으로 관리하기 편하게 제공해주는데요,
다음과 같이 메서드 시그니처 위에 선언 형식으로 사용할 수 있습니다.
@Tranactional
public void doOrder() {
productService.dcreaseCount();
orderService.saveOrder();
}
Transaction 속성
Spring에서 제공하는 트랜잭션 어노테이션은 트랜잭션 관리를 위한 다양한 속성을 제공해주는데, 우선 어노테이션의 내부를 살펴보면 다음과 같습니다.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
String[] label() default {};
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default -1;
String timeoutString() default "";
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
- value, transactionManager: 사용할 트랜잭션 매니저의 이름을 지정합니다. 두 필드는 서로의 별칭을 사용하며 같은 역할을 합니다.
- label: 트랜잭션에 레이블을 붙이는 데 사용됩니다. 주로 로깅이나 트랜잭션 모니터링과 같은 목적으로 사용됩니다.
- propagation: 트랜잭션의 전파(Propagation) 동작을 지정합니다. 작업이 새로운 트랜잭션에서 실행될지, 기존 트랜잭션에 참여할지 등을 결정합니다. 전파의 범위는 다음과 같습니다.
전파 동작 설명 REQUIRED
현재 트랜잭션이 존재하면 해당 트랜잭션에 참여하고, 없으면 새로운 트랜잭션을 시작합니다. SUPPORTS
현재 트랜잭션이 존재하면 참여하고, 없으면 트랜잭션 없이 실행됩니다. MANDATORY
반드시 기존 트랜잭션이 존재해야 하며, 트랜잭션이 없으면 예외를 발생시킵니다. REQUIRES_NEW
항상 새로운 트랜잭션을 생성하며, 기존 트랜잭션은 일시 중단됩니다. NOT_SUPPORTED
항상 트랜잭션 없이 실행되며, 기존 트랜잭션은 일시 중단됩니다. NEVER
트랜잭션 없이 실행되며, 트랜잭션이 존재하면 예외를 발생시킵니다. - isolation: 트랜잭션의 고립 수준(Isolation Level)을 지정하는데, 여러 트랜잭션 간 데이터 일관성 및 격리 정도를 제어합니다. 고립 수준은 다음과 같습니다.
고립 수준 설명 DEFAULT
기본 고립 수준이며, 대부분의 데이터베이스에서 기본값은 READ_COMMITTED
입니다.READ_UNCOMMITTED
다른 트랜잭션이 아직 커밋하지 않은 데이터도 읽을 수 있습니다. 흔히, Dirty Read가 허용됩니다. READ_COMMITTED
다른 트랜잭션이 커밋한 데이터만 읽기를 허용합니다. REPEATABLE_READ
트랜잭션 내에서 동일 쿼리 결과가 보장됩니다. SERIALIZABLE
가장 높은 고립 수준으로, 데이터를 읽거나 쓰기 전에 다른 트랜잭션이 접근하지 - timeout: 트랜잭션 실행 제한 시간을 초 단위로 설정합니다. 지정한 시간 내에 작업이 완료되지 않으면 롤백됩니다.
- timeoutString: 타임아웃 값을 문자열로 지정할 수 있습니다. 스프링 EL(Expression Language) 표현식을 사용 가능
- readOnly: boolean 값을 통해 트랜잭션이 읽기 전용인지 여부를 설정합니다.
- rollbackFor, rollbackForClassName: 트랜잭션을 롤백해야 할 예외 타입을 지정합니다. className의 경우 클래스 이름으로 롤백 예외를 지정할 수 있습니다.
- noRollbackFor: noRollbackForClassName: 트랜잭션을 롤백하지 않을 예외 타입을 지정합니다.
- className의 경우 클래스 이름으로 롤백하지 않을 예외를 지정할 수 있습니다.
동작원리
Spring의 트랜잭션은 @Transactional 어노테이션이 붙은 메서드를 Spring AOP를 통해 SpringTransactionAnnotationParser에서 해석하고, TransactionAspectSupport를 상속한 TransactionInterceptor 내부의 invoke 메서드가 호출되면서 트랜잭션 관리 기능이 동작하게 됩니다.
중요한 로직은 TransactionAspectSupport 내부에 존재하는데요, invoke 메서드가 호출되면 내부적으로 해당 클래스의 invokeWithinTransaction을 호출하게 됩니다.
이 메서드 내부에서는 다음과 같이 등록 된 TransactionManager의 설정을 가져와 분기 수행에 따라 다른 동작을 수행합니다.
아래 코드에서 Reactive 구조가 아닌 일반적인 MVC 구조에서는 PlatformTransactionManager를 사용하기 때문에 두 번째 분기문을 타게 되고, AOP에 의해 호출 된 클래스 정보와 메서드 정보를 통해 try-catch 블록에서 해당 메서드를 호출하게 됩니다.
추가적으로 해당 메서드 제일 하단에는 우리에게 익숙한 커밋 관련 메서드가 보이고, 해당 메서드 내부에서 트랜잭션에 대한 커밋을 처리하게 됩니다.
그렇다면, 트랜잭션에 대한 전파는 어디서 처리가될까요?
저는 사실 다른 코드보다 이 부분이 제일 궁금했는데요, 이전에 invokeWithinTransaction 메서드에서 일반적인 MVC의 경우 PlatformTransactionManager를 사용한다고 했었는데 해당 트랜잭션 정보를 가져오는 다음 메서드 내부에서 AOP에 의해 정의 된 트랜잭션 설정 정보에 따라 다음과 같이 분기 처리하여 트랜잭션을 유지 혹은 생성하게 됩니다.
정리
트랜잭션을 단순히 사용하기만 했던 때와 달리, 이번에 내부 흐름을 살펴보며 많은 것을 배울 수 있었는데요,
흐름을 알아보다 보니 Reactive 환경에서는 처리가 따로 진행되는 것 같아 흥미로웠는데요, 시간이 된다면 비동기 처리 부분도 분석해서 정리해보고 싶네요.
'Spring Framework > spring' 카테고리의 다른 글
[SpringBoot] CORS의 이해와 설정 (0) | 2025.01.21 |
---|---|
[SpringBoot] Spring 버전 마이그레이션 (0) | 2025.01.13 |
[SpringBoot] Spring FIiter (0) | 2024.12.29 |
[SpringBoot] ApplicationEvent를 활용한 이벤트 발행/구독 (0) | 2024.12.28 |
[SpringBoot] @ComponentScan 동작 원리 (0) | 2024.12.09 |