@Transactional 어노테이션 알아보기[1]
들어가기에 앞서
스프링은 선언적 트랜잭션인 @Transactional를 통해서 트랜잭션을 사용해야 하는 많은 코드들에 청량함을 부여했다. @Transactional이 어떤방식으로 동작하는지를 알지 못해도 사용할 수 있을정도로 쉽게 사용할 수 있지만 사용할 때 주의해야 할 특징들과 주의 사항들이 몇 가지 존재한다.
@Transactional 사용방법
@Transactional을 사용하는 방법은 트랜잭션을 적용하고자 하는 클래스 또는 메소드에 해당 어노테이션을 선언해주면 된다.
class BasicService {
@Transactional
fun tx() {
logInfo("call tx")
val txActive = TransactionSynchronizationManager.isActualTransactionActive()
logInfo("tx active=${txActive}")
}
fun nonTx() {
logInfo("call nonTx")
val txActive = TransactionSynchronizationManager.isActualTransactionActive()
logInfo("tx active=${txActive}")
}
}
위의 코드처럼 트랜잭션을 특정 메소드에만 걸고싶다면 메소드 위에 @Transactional을 선언해주면 된다. 위의 코드에서 사용하고 있는 isActualTransactionActive() 메소드는 현재 트랜잭션을 사용하고 있는지 확인하는 메소드이다. 만약 클래스 안에 여러개의 메소드가 있고 모든 메소드에 트랜잭션을 적용하고 싶다면 클래스 레벨에 선언할 수도 있다.
@Transactional
class BasicService {
fun tx() {
logInfo("call tx")
val txActive = TransactionSynchronizationManager.isActualTransactionActive()
logInfo("tx active=${txActive}")
}
fun nonTx() {
logInfo("call tx2")
val txActive = TransactionSynchronizationManager.isActualTransactionActive()
logInfo("tx active=${txActive}")
}
}
위의 코드를 실행시켜보면 두 메소드 모두 트랜잭션이 사용중이라는 결과를 볼 수 있다.
@Transactional선언의 우선순위
스프링에서는 모든 우선순위가 동일하게 적용되는데 바로 구체적일수록 우선순위가 높다 이다. 클래스에 선언한 것보다 메소드에 선언한 것이 더 높은 우선순위를 가진다.
이러한 특징을 이용해서 클래스 레벨에서 @Transactional(readOnly = true)를 사용하고 데이터의 변화가 필요한 메소드에서는 @Transactional를 따로 선언해서 사용하기도 한다. @Transactional의 readOnly 기본값은 false이기 때문에 @Transactional만 선언해도 된다.
@Transactional 사용 시 주의사항
@Transactional어노테이션을 사용할 떄 주의해야 할 것들이 몇 가지 존재한다.
- 내부 호출은 트랜잭션이 적용되지 않을 수도 있다.
- @Transactional어노테이션은 public메소드에만 적용된다.
- 초기화 코드에 @Transactional을 선언하면 트랜잭션이 적용되지 않는다.
(1) 내부 호출은 트랜잭션이 적용되지 않을 수도 있다.
표현만 놓고 본다면 조금 이상하다는 생각이 들 수도 있다. 조금 더 자세히 표현해보면 다음과 같은 조건들이 있다.
- 하나의 클래스에 2개의 메소드가 있다.
- 처음 외부에서 호출된 메소드는 @Transactional이 적용되있지 않다.
- 처음 호출된 메소드가 내부의 @Transactional이 걸려있는 다른 메소드를 호출한다.
이런 상황이면 3번에서 호출된 메소드에는 트랜잭션이 적용되지 않는다. 조금 더 이해하기 쉽게 코드로 살펴보자.
class CallService {
fun external() {
logInfo("call external")
printTxInfo()
internal()
}
@Transactional
fun internal() {
logInfo("call internal")
printTxInfo()
}
private fun printTxInfo() {
val txActive = TransactionSynchronizationManager.isActualTransactionActive()
logInfo("tx active=$txActive")
}
}
// 1. 바로 internal함수 호출
@Test
fun internalCall() {
callService.internal()
}
// 2. external함수를 거쳐서 internal함수 호출
@Test
fun externalCall() {
callService.external()
}
이런 코드일 것이다.
위의 코드에서 1번을 호출하면 바로 트랜잭션이 적용된 결과가 나타날 것이다.
: Getting transaction for [hello.springtx.apply.InternalCallV1Test$CallService.internal]
: call internal
: tx active=true
: Completing transaction for [hello.springtx.apply.InternalCallV1Test$CallService.internal]
로그 결과를 살펴보면 트랜잭션이 잘 적용된 것을 볼 수 있다.
2번을 호출하는 경우에는 처음 외부에서 호출받은 메소드는 @Transactional 적용되어 있지 않기 때문에 트랜잭션이 걸리지 않을 것이다. 그리고 이 메소드가 호출하는 내부 메소드도 트랜잭션이 걸리지 않는데 internal메소드를 실행할 때 this.internal의 형식으로 호출하기 때문이다. 내부 메소드를 호출 할때는 앞에 this가 생략되서 보이지 않지만 항상 this가 포함되서 실행된다.
: call external
: tx active=false
: call internal
: tx active=false
실행시켜보면 위와 같은 로그가 출력된다. external과 internal이 둘 다 트랜잭션이 걸리지 않은 모습이다.
(2) @Transactional어노테이션은 public메소드에만 적용된다.
제목 그대로 @Transactional어노테이션은 public에서만 적용이 된다. 추측해보자면 일반적으로 트랜잭션이 걸리는 위치는 비즈니스 로직의 시작일 것이고 이 위치는 보통 외부에서 호출되는 시점일 것이다. 그리고 private, protected 등과 같은 곳은 보통 트랜잭션을 적용하고자 하지 않을 것이다.
하지만 @Transactional이 public외의 곳에서 선언이 안되는 것은 아니다. private에 선언해도 컴파일에러는 나타나지 않고 런타임에서 적용되지 않을 뿐이다. 그렇기 때문에 '나는 적용했는데 왜 적용이 안됐지?' 라는 생각을 하지 않도록 조심해야 한다.
(3) 초기화 코드에 @Transactional을 선언하면 트랜잭션이 적용되지 않는다.
@PostConstruct를 사용하면 스프링이 초기화되는 시점에 코드를 동작시킬 수 있다. 개발을 하다보면 이 기능을 사용해야되는 시점이 있을 수 있다. 하지만 여기에 @Transactional을 선언해도 트랜잭션은 적용되지 않는다.
이유는 스프링 애플리케이션의 실행 순서 때문인데, @Transactional가 AOP의 원리로 동작되는 만큼 AOP가 모두 동작해야 트랜잭션이 동작할 수 있는데 @PostConstruct가 실행되는 시점이 AOP가 동작하는 시점보다 빠르기 때문이다.
이 문제를 해결하기 위해서 @EventListener(ApplicationReadyEvent::class)를 사용할 수 있는데 해당 어노테이션과 옵션을 사용하면 애플리케이션이 모두 실행된 이후에 해당 코드가 실행되도록 만들 수 있다.
class Hello {
@PostConstruct
@Transactional
fun intiV1() {
val isActive = TransactionSynchronizationManager.isActualTransactionActive()
logInfo("Hello init @PostConstruct tx active=$isActive")
}
@EventListener(ApplicationReadyEvent::class)
@Transactional
fun initV2() {
val isActive = TransactionSynchronizationManager.isActualTransactionActive()
logInfo("Hello init @PostConstruct tx active=$isActive")
}
}
위의 코드를 실행시키면 다음과 같은 로그가 나타난다.
...
Hello init @PostConstruct tx active=false -- 트랜잭션이 적용되지 않았다.
Started InitTxTest in 1.143 seconds (process running for 1.634)
Getting transaction for [hello.springtx.apply.InitTxTest$Hello.initV2]
Hello init @PostConstruct tx active=true -- 트랜잭션이 적용되었다.
Completing transaction for [hello.springtx.apply.InitTxTest$Hello.initV2]
코드를 보면 트랜잭션이 적용되지 않은 로그와 적용된 로그를 둘 다 볼 수 있다.
스프링의 실행순서까지 잘 고려해서 @Transactional어노테이션을 사용해야 한다.
'Spring > Core' 카테고리의 다른 글
스프링과 데이터베이스[2] - 트랜잭션 사용 (0) | 2022.11.09 |
---|---|
스프링과 데이터베이스[1] - JDBC와 커넥션 풀 (0) | 2022.11.08 |
스프링 주입의 다양한 방법들 (0) | 2022.06.13 |
스프링 빈 등록의 다양한 방법들 (0) | 2022.06.09 |
[3/3] Spring과 싱글톤 패턴 - @Configuration의 비밀 (0) | 2022.06.07 |
댓글
이 글 공유하기
다른 글
-
스프링과 데이터베이스[2] - 트랜잭션 사용
스프링과 데이터베이스[2] - 트랜잭션 사용
2022.11.09 -
스프링과 데이터베이스[1] - JDBC와 커넥션 풀
스프링과 데이터베이스[1] - JDBC와 커넥션 풀
2022.11.08 -
스프링 주입의 다양한 방법들
스프링 주입의 다양한 방법들
2022.06.13 -
스프링 빈 등록의 다양한 방법들
스프링 빈 등록의 다양한 방법들
2022.06.09