@Transactional
스프링 프레임워크에서 트랜잭션 관리를 위해 사용하는 애노테이션으로, 메소드 또는 클래스에 적용할 수 있다.
이 애노테이션이 적용된 메소드가 실행될 때, 해당 메소드 내의 작업을 트랜잭션으로 묶어주는 역할을 한다.
기존에는 public 메서드에만 트랜잭션이 적용되었지만 스프링 6.0 부터는 protected와 default 메서드에도 트랜잭션이 적용되도록 바뀌었다.
인터페이스에도 애노테이션을 적용할 수 있지만 가급적 권장하지 않는다.
Method visibility and @Transactional in proxy mode The @Transactional annotation is typically used on methods with public visibility. As of 6.0, protected or package-visible methods can also be made transactional for class-based proxies by default. Note that transactional methods in interface-based proxies must always be public and defined in the proxied interface. For both kinds of proxies, only external method calls coming in through the proxy are intercepted.
@Transactional 애노테이션이 클래스, 메서드에 하나라도 붙어있다면 해당 클래스는 트랜잭션 적용 대상이 되어 프록시 방식의 AOP가 적용되며, 해당 프록시 객체가 스프링 빈에 등록된다.
프록시 적용 시 메서드 내부 호출 문제
@Transactional을 사용하면 프록시 객체가 요청을 받아서 처리하고 실제 객체를 호출한다.
그렇다면 다음과 같은 상황은 어떻게 될까?
outerMethod에는 @Transactional이 없고 outerMethod에서 호출하는 innerMethod()에는 존재한다.
@Slf4j
static class TestService {
public void outerMethod() {
log.info("outerMethod 시작 {}", TransactionSynchronizationManager.isActualTransactionActive());
innerMethod();
}
@Transactional
public void innerMethod() {
log.info("innerMethod 시작 {}", TransactionSynchronizationManager.isActualTransactionActive());
}
}
@Test
@DisplayName("내부메서드_직접_호출")
void innerCall() {
testService.innerMethod(); // innerMethod 시작 true
}
@Test
@DisplayName("외부에서_내부메서드_호출")
void outerCall() {
testService.outerMethod(); // outerMethod 시작 false, innerMethod 시작 false
}
프록시에서 메서드가 호출되는 것이 아니라 대상 객체의 내부에서 @Transactional 메서드를 직접 호출하는 경우에는 트랜잭션이 적용이 되지 않는다.
이러한 이유는 트랜잭션은 프록시 객체를 통하여 적용이 되는데 프록시 객체를 통해 호출되는 outerMethod()는 트랜잭션이 적용이 되지 않았기 때문이다.
즉 프록시 객체 → 대상 객체 TestService.outerMethod() 호출 → 내부에서 직접 this.innerMethod()를 호출하므로 트랜잭션 적용이 되지 않는다.
이런 경우 프록시를 적용하고 싶다면 트랜잭션을 적용하는 innerMethod()를 별도의 클래스로 분리한다.
@Slf4j
static class TestService {
@Autowired InnerService innerService;
public void outerMethod() {
log.info("outerMethod 시작 {}", TransactionSynchronizationManager.isActualTransactionActive());
innerService.innerMethod();
}
}
@Slf4j
static class InnerService {
@Transactional
public void innerMethod() {
log.info("innerMethod 시작 {}", TransactionSynchronizationManager.isActualTransactionActive());
}
}
이렇게 하면 TestService가 프록시 InnerService를 호출하고 트랜잭션이 적용된다.
참고로 @Transactional이 하나라도 붙어있다면 프록시 객체가 빈으로 등록되고,
@PostConstruct, @Transactional 을 함께 사용하면 트랜잭션이 적용되지 않는다. (초기화 이후 AOP가 적용되므로)
→ @EventListener(value = ApplicationReadyEvent.class), @Transactional 를 사용하면 스프링 컨테이너가 완전히 생성되고 난 이후 호출된다.
@Transactional의 특성
@Transactional 애노테이션은 로직이 성공적으로 수행되면 커밋하도록 동작한다. 그런데 예외가 발생한다면 다음과 같이 동작한다.
- 언체크 예외인 RuntimeException , Error 와 그 하위 예외 발생 시 롤백.
- 체크 예외인 Exception 과 그 하위 예외 발생 시 커밋.
만약 어떤 체크 예외 발생 시 롤백하고 싶다면 @Transactional(rollbackFor = 예외클래스명.class) 지정
이렇게 하면 해당 예외가 하위 예외가 발생하면 롤백을 한다.
테스트에서의 @Transactional
@Transactional이 테스트에 있으면 스프링은 테스트를 트랜잭션 안에서 실행하고, 테스트가 끝나면 트랜잭션을 자동으로 롤백시킨다.
만약 커밋을 원한다면 @Commit, @Rollback(value = false)을 추가하면 된다.
스프링 트랜잭션 전파
그렇다면 트랜잭션이 둘 이상 존재한다면 어떻게 될까? (트랜잭션이 진행 중인데 추가로 트랜잭션을 수행할 때)
다음 상황을 살펴보자.
@Slf4j
@SpringBootTest
public class PropagationTest {
@Autowired PlatformTransactionManager transactionManager;
@Test
void inner_commit() {
log.info("외부 트랜잭션 시작");
TransactionStatus outerTx = transactionManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outerTx.isNewTransaction());
log.info("내부 트랜잭션 시작");
TransactionStatus innerTx = transactionManager.getTransaction(new DefaultTransactionAttribute());
log.info("inner.isNewTransaction()={}", innerTx.isNewTransaction());
log.info("내부 트랜잭션 커밋");
transactionManager.commit(innerTx);
log.info("외부 트랜잭션 커밋");
transactionManager.commit(outerTx);
}
@TestConfiguration
static class Config {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
}
외부 트랜잭션 시작
outer.isNewTransaction()=true
내부 트랜잭션 시작
inner.isNewTransaction()=false
내부 트랜잭션 커밋
외부 트랜잭션 커밋
외부에서 트랜잭션을 시작하고 커밋을 하지 않았는데 트랜잭션을 추가로 수행했다.
이 때 신규 트랜잭션을 확인하는 isNewTransaction의 결과는 외부는 true, 추가 수행된 트랜잭션은 false로 나온다.
즉 내부 트랜잭션이 먼저 진행한 트랜잭션에 참여하여 외부와 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶인다. 실질적으로 하나의 물리 트랜잭션이지만 내부에는 두 개의 논리 트랜잭션으로 나뉘어져있다.
- 물리 트랜잭션 : 실제로 데이터베이스에 적용되는 트랜잭션
- 논리 트랜잭션 : 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위로 트랜잭션 진행 중 추가로 트랜잭션을 사용하는 경우에 사용
모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋.
하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백.
그렇다면 내부 트랜잭션에서 롤백이 된다면?
논리 트랜잭션 중 하나라도 롤백이 되었기 때문에 해당 요청은 모두 롤백된다.
위 그림을 보면 내부 트랜잭션은 커밋, 롤백을 할 때 아무 것도 하지 않는다고 하였다.
왜냐하면 내부에서 실제로 커밋이나, 롤백을 하면 외부 트랜잭션까지 이어지지 못하고 끝나버리기 때문이다.
따라서 실제로 커밋, 롤백이 일어나는 것은 외부 트랜잭션일 경우이다.
그렇다면 내부 트랜잭션에서 롤백이 일어났는지 외부 트랜잭션은 어떻게 확인을 할까?
위 코드에서 내부 트랜잭션이 롤백을 하도록 코드를 수정해보자.
log.info("내부 트랜잭션 커밋");
transactionManager.rollback(innerTx);
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
그렇다면 다음과 같이 UnexpectedRollbackException이 발생한다.
이는 내부 트랜잭션은 롤백하는 경우 실제 롤백을 호출하는 것이 아니라 트랜잭션 동기화 매니저에 rollbackOnly=true를 설정하고 다음 트랜잭션으로 넘긴다.
외부 트랜잭션은 rollbackOnly 옵션을 확인 후 true로 설정되어 있다면 실제로 롤백 후 UnexpectedRollbackException 런타임 예외를 발생시키며, 그렇지 않다면 커밋을 한다.
REQUIRES_NEW
그렇다면 외부 트랜잭션과 내부 트랜잭션을 구분하려면 어떻게 해야할까?
REQUIRES_NEW 옵션을 적용하면 트랜잭션을 완전히 따로 실행시킬 수 있다.
@Test
void inner_commit() {
log.info("외부 트랜잭션 시작");
TransactionStatus outerTx = transactionManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outerTx.isNewTransaction());
log.info("내부 트랜잭션 시작");
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus innerTx = transactionManager.getTransaction(attribute);
log.info("inner.isNewTransaction()={}", innerTx.isNewTransaction());
log.info("내부 트랜잭션 커밋");
transactionManager.commit(innerTx);
log.info("외부 트랜잭션 커밋");
transactionManager.commit(outerTx);
}
이렇게 하면 외부와 내부 트랜잭션으 isNewTransaction()=true로 기존 트랜잭션에 참여하는 것이 아니라 새로 시작하는 것을 알 수 있다.
즉 REQUIRES_NEW 옵션으로 내부 트랜잭션을 시작하면 트랜잭션 매니저는 데이터소스를 통해 커넥션을 생성하고 생성한 커넥션을 수동 커밋으로로 설정하여 물리 트랜잭션 시작한다.
그리고 트랜잭션 동기화 매니저에 해당 커넥션을 보관하고 내부 트랜잭션이 완료될 때 까지 기존 커넥션은 보류된다. (REQUIRES_NEW를 사용하면 커넥션은 다르고, 동일 쓰레드에서 진행) 내부 트랜잭션이 커밋, 롤백되어 종료되면 기존 커넥션을 다시 사용한다.
이 경우 데이터베이스 커넥션이 동시에 2개 사용되는 것에 유의해야 한다.
참고
스프링 DB 2편 - 데이터 접근 활용 기술 강의 | 김영한 - 인프런
김영한 | 백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔드
www.inflearn.com
Using @Transactional :: Spring Framework
The @Transactional annotation is metadata that specifies that an interface, class, or method must have transactional semantics (for example, "start a brand new read-only transaction when this method is invoked, suspending any existing transaction"). The de
docs.spring.io
'Spring' 카테고리의 다른 글
[Spring AOP] 동적 프록시 (0) | 2024.12.28 |
---|---|
[Spring] ThreadLocal (0) | 2024.12.28 |
[Spring] JDBC, 커넥션 풀, 트랜잭션 추상화 (0) | 2024.12.27 |
[Spring] MultipartFile 바인딩 (0) | 2024.12.27 |
[Spring] 스프링의 예외 처리 (0) | 2024.12.27 |