✅ 트랜잭션의 네 가지 속성
🔹트랜잭션 전파(Transaction Propagation)
A의 트랜잭션이 실행되고 있을 때, B의 트랜잭션이 별개로 시작되고 있다. 이러한 경우 어떻게 해야 할까?
🔻PROPAGATION_REQUIRED (Default)
- 진행 중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있으면 이에 참여한다
DefaultTransactionDefinition의 기본 값이다. B는 A의 트랜잭션에 참여하기 때문에. A의 (2)번 위치에서 예외가 발생하면, B는 취소된다.
🔻PROPAGATION_REQUIRES_NEW
- 항상 새로운 트랜잭션을 시작한다
A, B는 각각 새로운 트랜잭션을 시작하기 때문에 독립적인 트랜잭션이 된다. 따라서 A의 (2)번 위치에서 예외가 발생해도, B는 커밋된다
🔻PROPAGATION_MANDATORY
- 진행 중인 트랙잭션이 있으면 참여하고, 없으면 예외를 발생시킨다
B는 A가 트랜잭션이 활성화되어 있으면 동일한 트랙잭션에서 수행되고, 없으면 예외가 발생한다. 독립적으로 트랜잭션을 진행하면 안 되는 경우에 사용한다.
🔻PROPAGATION_NOT_SUPPORTED
- 현재 트랜잭션이 존재하면 트랜잭션을 일시 중단한 다음 트랜잭션 없이 비즈니스 로직 실행
특정 비즈니스 로직이 트랜잭션 없이 동작하도록 할 때 사용하며, 진행중인 트랜잭션이 있어도 무시한다.
🔹 격리수준(Isolation Level)
@Transactional(isolation = Isolation.XXX)
public void example(String message) {
// ...
}
격리는 원자성(Atomicity), 일관성(Consistency), 격리(Isolation) 및 내구성(Durability)의 일반적인 ACID 특성 중 하나이다. 트랜잭션을 동시에 진행시키면서도 서로 간의 독립적인 작업을 유지하는지 나타낸다.
➡️ 격리 수준은 아래와 같은 동시성 부작용을 예방할 수 있다.
- Dirty read : 동시에 실행되는 트랙잭션에서 커밋되지 않은 변경 내용을 읽는다. 트랜잭션 A가 어떤 값을 1에서 2로 변경하고 아직 커밋하지 않은 상황에서 트랜잭션B가 같은 값을 읽는 경우 트랜잭션 B는 2가 조회 된다.
- Nonrepeatable read : 트랜잭션 A가 특정 값을 읽은 후, 동시에 실행되는 트랙잭션 B가 동일한 값을 수정하고 커밋할 때, 트랜잭션 A가 값을 다시 읽을 때 다른 값을 가지고 오는 문제.
- Phantom read : 트랜잭션 A가 특정 범위의 Row를 읽은 후, 다른 트랜잭션 B가 일부 Row를 추가 또는 제거하고 커밋하는 경우, 트랜잭션 A가 같은 쿼리를 다시 수행할 때 다른 결과를 가지고 온다. 한 트랜잭션에서 일정 범위의 레코드를 두 번 이상 읽을 때 발생하는 데이터 불일치이다.
🔻DEFAULT
- DBMS의 기본 격리 수준 적용
🔻READ_UNCOMMITED (level 0)
- 트랜잭션의 동시 액세스 허용
- 세 가지 동시성 부작용이 모두 발생 (Dirty read, Nonrepeatable read, Phantom read)
- Postgres는 미지원(대신 READ_COMMITED 로 폴백), Oracle은 지원하거나 허용하지 않음
🔻READ_COMMITED (level 1)
- Dirty read 방지
- 나머지 부작용은 여전히 발생할 수 있음 (Nonrepeatable read, Phantom read)
- Postgres, SQL Server 및 Oracle의 기본 수준
🔻REPEATEABLE_READ (level 2)
- Dirty read, Nonrepeatable read 방지
- 업데이트 손실을 방지하기 위해 필요한 가장 낮은 수준 (동시 액세스를 허용하지 않음)
- Phantom read 부작용은 여전히 발생
- MySQL의 기본 수준, Oracle은 미지원
🔻SERIALIZABLE (level 3)
- 가장 높은 격리 수준이지만, 동시 호출을 순차적으로 실행하므로 성능 저하의 우려
- 모든 부작용을 방지
👉 DefaultTransactionDefinition에 설정된 격리수준 ISOLATION_DEFAULT
이다. 이는 DMBS에 따라 DataSource에 설정된 디폴트 격리수준을 따른다.
🔹제한 시간(Timeout)
@Transactional(timeout=10) // 단위: second
트랜잭션을 수행하는 제한시간을 설정한다. 지정한 시간 내에 해당 메소드 수행이 완료되이 않은 경우 rollback 수행한다. DefaultTransactionDefinition의 기본설정은 제한시간이 없다. 제한시간은 다른 트랜잭션에 참여하는 것이 아닌, 트랜잭션을 직접 시작할 수 있는 속성인 PROPAGATION_REQUIRED나 PROPAGATION_REQUIRES_NEW와 함께 사용해야 적용된다.
🔹읽기 전용(Read Only)
@Transactional(readOnly = true)
트랜잭션을 읽기 전용으로 설정한다. 성능을 최적화하기 위해 사용하거나, 특정 트랜잭션 작업 안에서 쓰기 작업이 일어나는 것을 의도적으로 방지하기 위해 사용한다. readOnly = true
인 경우 INSERT, UPDATE, DELETE 실행 시 예외 발생하며, 기본값은 false이다. (default : false)
✅ 트랜잭션 어노테이션(Transaction Annotation)
스프링에서는 트랜잭션 처리를 지원하는데 그중 어노테이션 방식으로 @Transactional을 선언하여 사용하는 방법이 일반적이며, 선언적 트랜잭션이라 부른다. 설정파일에서 패턴으로 분류 가능한 그룹을 만들어서 일괄적으로 속성을 부여하는 대신에 직접 타겟(클래스, 메소드, 인터페이스)에 트랜잭션 속성정보를 가진 어노테이션을 지정한다. 타겟의 Class 정보를 통해서 프록시 객체를 생성하여 트랜잭션을 시작하고, Commit 또는 Rollback 한다.
🔹@Transactional
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
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 TransactionDefinition.TIMEOUT_DEFAULT;
String timeoutString() default "";
// 읽기/쓰기 vs 읽기 전용 트랜잭션
boolean readOnly() default false;
// 롤백이 수행되어야 하는, 선택적인 예외 클래스의 배열
Class<? extends Throwable>[] rollbackFor() default {};
// 롤백이 수행되어야 하는, 선택적인 예외 클래스 이름의 배열
String[] rollbackForClassName() default {};
// 롤백이 수행되지 않아야 하는, 선택적인 예외 클래스의 배열
Class<? extends Throwable>[] noRollbackFor() default {};
// 롤백이 수행되지 않아야 하는, 선택적인 예외 클래스 이름의 배열
String[] noRollbackForClassName() default {};
}
@Transactional이 부여된 모든 오브젝트를 자동으로 타겟으로 인식한다. 이때 사용되는 포인트 컷은 TransactionAttributeSourcePointcut 이다. TransactionInterceptor는 AnnotationTransactionAttributeSource를 활용해서 @Transactional을 포인트컷으로 인식한다.
🔹@Transactional Fallback Policy
코드가 복잡해지는 것을 방지하기 위해, 4단계의 Fallback 정책을 제공한다Fallback 정책이란, 쉽게말해서 메소드가 클래스에 포함되있을 경우, 메소드에 설정된 어노테이션이 없다면, 클래스에 설정된 어노테이션을 적용한다
🔻스프링 @Transactional 어노테이션 적용 순서
1. 타깃 오브젝트의 메소드
2. 타깃 오브젝트의 타입(클래스)
3. 타깃이 구현한 인터페이스의 메소드
4. 타깃이 구현한 인터페이스의 타입
👉 구현체의 우선 순위가 높다고 기억하자
스프링은 클래스의 Method 레벨인 [5], [6]번의 @Transactional을 먼저 적용하고, 만약 없다면, Class 레벨인 [4]번 @Transactional을 안에 있는 메소드들 중, @Transactional이 없는 메소드들에게 적용한다. 만약 [4]번에도 없다면, 인터페이스의 메소드인 [2], [3]을 적용하고, [2], [3]에도 속하지 않는다면, [1]에 있는 어노테이션을 적용한다.
✅ 프록시 방식 AOP는 같은 타겟 오브젝트 내의 메소드를 호출할 때는 적용되지 않는다.
프록시 방식의 AOP에서는 프록시를 통한 부가기능의 적용은 클라이언트로부터 호출이 일어날 때만 가능하다. 반대로 타깃 오브젝트가 자기 자신의 메소드를 호출할 때는 프록시를 통한 부가기능의 적용이 일어나지 않는다.
타겟 클래스의 메소드인 delete() 메소드 내에서 update() 메소드를 호출하는 경우[2], 트랜잭션이 전파되지 않는다. delete() 메소드가 트랜잭션을 시작하거나, 앞선 트랜잭션에 참여했기 때문에 전파될 것 같다. 하지만 프록시 방식의 AOP는 프레임워크가 프록시 객체의 메소드 호출을 통해서 트랜잭션을 시작하거나 전파한다. 따라서 타겟 오브젝트 내의 메소드를 호출할 때는 AOP 프레임워크가 해당 메소드의 호출을 감지하기 어려워 트랜잭션이 활성화되지 않는다.
➡️ 해결방법
- 스프링의 API를 사용해 같은 객체의 메소드 호출도 프록시를 이용하도록 강제한다.
- AspectJ와 같은 타겟의 바이트코드를 직접 조작하는 방식의 AOP 기술을 적용한다.
🏷️이미지 출처 및 참고한 사이트
Uploaded by N2T