지난글...
https://kyeum-d.tistory.com/35
[Transactional] 거래가 있었는데요.. 없었습니다
거래내역의 실종입사하자마자 흥미로운 주제의 CS가 들어왔습니다. 몇달 동안 지속적으로 유입되던 문의였습니다. 내용은 "거래를 분명히 했고 잘 됐다고 떳는데요.." "거래내역에 제 거래가
kyeum-d.tistory.com
지난 포스팅에서 @Transactional 이 붙어있음에도 DB Stored Procedure 의 Rollback 을 감지하지 못하고
이후의 프로세스를 처리하는 문제를 식별했습니다.
이번 포스팅에서는 Transactional 이 어떻게 동작하는지 이해하고 왜 이런 현상이 발생했는지 분석하는 시간을 가져보겠습니다.
* 이번 포스팅은 TransactionManager의 구현체로 JPA와 MSSQL 기준으로 작성 된 글입니다.
@Transactional 너 어떻게 동작하는거지?
Transactional은 기본적으로 관심사의 분리로 인해 AOP로 동작합니다. 이 것을 Spring이 굉장히 단순화 시켜서 사용자들에게 추상화된 레벨의 어노테이션으로 기능을 제공하고 있습니다.
그렇다면 어떻게 트랜잭션을 맺고 관리하는 지 알아보기 위해
Transacional의 주요 동작이 일어나는 Manager 에서의 동작을 살펴보겠습니다. (동기화 부분은 이번 파트에서 제외)
Spring 의 핵심적인 가치와 걸맞게 이 Transaction을 관리하는 Manager 또한 추상화 되어있습니다.
PlatformTransactionManager 를 통해 추상화 하고 각 기술 별로 Manager를 구현하여 사용하는 구조로 이루어져있습니다.
JPA에서는 JpaTransactionManager 가 이 PlatformTransactionManager를 구현하여 트랜잭션을 관리합니다.
동작은 크게
1. Transaction 을 시작하는 doGetTrasnaction()
2. Transaction을 commit하는 doCommit()
3. Transaction을 rollback하는 rollback()
3가지로 이루어져있습니다.
디버깅을 통해 세가지 동작에 대해서 실제로 어떤 일이 일어나는지 알아보겠습니다.
doGetTransaction()
최초 커넥션을 맺는 역할을 합니다.
AbstractPlatformTransactionManager 클래스의 getTransaction() 으로부터 시작합니다.
해당 메서드 2번째 라인에서 구현체의 doGetTransaction()을 호출하는 부분이 보입니다.
아래 분기문에서 Transaction이 이미 존재하는 경우와 아닌 경우를 구분해서 판단 하는 것을 알 수 있습니다.
지금은 최초 트랜잭션을 맺는 과정임으로
this.startTransaction 메서드를 실행하는 것을 확인 할 수 있습니다.
startTransaction 에서는 다시 doBegin 메서드를 호출하는 모습
여기서 this.doBegin 은 JPA TransactionManager구현체의 doBegin을 실행합니다.
doBegin에서는 여러 설정들을 하며 this.getJpaDialect().beginTransaction 으로 Transaction을 시작합니다.
여기서 JpaDialect의 구현체는 HibernateDialect의 begin() 을 실행합니다.
setAutoCommit을 false로 설정하는 모습
HikariProxyConnection에서 setAutoCommit 을 실행
setAutoCommit에서는 connectionCommand 를 통해 sql 문을 실행합니다.
여기서 생성된 sql문은
"set implicit_transactions on" 으로
MSSQL에서 implicit_transactions 를 on 으로 하겠다는 의미는
데이터 변경을 일으킬 수 있는 다음과 같은 SQL 명령어를 실행할 때마다 트랜잭션이 자동으로 시작된다는 것을 의미합니다.
• INSERT
• UPDATE
• DELETE
• MERGE
• CREATE
• DROP
• ALTER
이 명령에서 트랜잭션이 시작되며, 사용자가 명시적으로 COMMIT이나 ROLLBACK을 실행할 때까지 트랜잭션이 종료되지 않습니다.
많이 돌아왔지만 결국 transaction을 시작 하는 것은 set implicit_transactions on 처리를 하기 위함이였음이 밝혀졌습니다.
여기서부터 벌써 제가 생각한 것과는 다르게 동작하는 것을 확인했습니다...
제 어렴풋한 예상으로는 db transaction을 시작시키고 현재 transaciton id 를 가지고 있다가 이 id가 변경되거나 했을 때(SP에서 rollback) 변경도 감지 할 수 있을 것으로 예상 했으나
이런 방식의 동작이라면 SP에서 rollback 을 충분히 감지하지 못할 것으로 판단이 되었습니다.
그렇다면 rollback은 어떻게 일어나는 것일까요?
doRollback()
TransactionAspectSupport 클래스에서 Exception 처리에 대해 AfterThrowing을 실행시키고
completeTransactionAfterThrowing 에서 rollback 메서드를 실행하는 것을 확인 할 수 있습니다.
doGetTransaction과 동일하게 추적 한 결과
결국 실행하는 쿼리는
"IF @@TRANCOUNT > 0 ROLLBACK TRAN"
해당 명령어는
@@TRANCOUNT 으로 현재 활성화 된 Transaction 이 있는지 확인하고 있다면 ROLLBACK 시키는 것을 의미합니다.
결국 트랜잭션이 필요한 동작이 있을 때 Transaction을 새로 생성하고 Exception이 나는 경우 현재 생성된 트랜잭션이 있는지 파악하고 ROLLBACK 시키는 역할을 해주는 것입니다.
따라서, 현재 상황을 종합 해 봤을 때 SP에서 일어나는 Exception은 당연히 Application에 전파되지 않고
SP에서 ROLLBACK 명령어는 실행했기 때문에 DB에서는 현재 트랜잭션을 ROLLBACK 시켰고
Application 에서는 다음 프로세스를 시작 할 때 다시 트랜잭션을 생성해서 나머지 작업을 진행 한 것으로 보이는 상황입니다.
그렇다면, 새로운 Transaction을 생성하는지 비교해보자
가설을 세웠으니 검증하는 시간을 가져보겠습니다.
두가지 상황을 두고 각 상황을 비교해보겠습니다.
- 첫번째 상황
- 주문 내역 정보 저장
- 포인트 프로시저 실행 (정상)
- 쿠폰 내역 정보 저장
- 두번째 상황
- 주문 내역 정보 저장
- 포인트 프로시저 실행 (실패 후 롤백)
- 쿠폰 내역 정보 저장
이렇게 두가지 상황에서
- 쿠폰 내역 정보 저장
하기 작전에 doGetTransaction을 살펴보고 어떤 차이가 있는지 확인해보겠습니다.
첫번째 상황 (정상)
테스트를 실행하고 디버깅을 합니다.
예상 했던 대로 isExistingTransaction 을 통과하여 이미 존재하는 Transaction에 합류합니다.
두번째 상황 (롤백)
예상과는 달리 isExistingTransaction을 통과하고 합류하는 것을 확인 할 수 있습니다.
그렇다면?
어떤 기준으로 ExistingTransaction을 하는지 확인해봐야합니다.
hasTransaction() 메서드를 통해 판단하고 있고
hasTransaction 은 isTransactionActive() 메서드를 통해 판단하고 있습니다.
결국엔 EntityManagerHolder의 transactionActive 필드의 true/false 여부로 판단하고 있는 것을 확인 할 수 있습니다.
이것은 무엇을 의미할까요?
그렇습니다. SP에서는 EntityManagerHolder의 transactionActive 값을 변경 해 줄 수 없고
그렇다고 EntityManagerHolder가 DB에서 rollback 되었는지 여부를 알 수도 없으니 당연한 결과였습니다..
결국 예상과는 다르게 SP에서 일어난 모든 동작에 대해 Application에서는 알 수단도 방법도 없었으며
(어찌보면 당연한..)
용빼는 재주가 없음
최초 set implicit_transactions on 처리 해 둔 것으로 미루어보아
다시 transaction이 필요한 작업 실행 시
DB에서 새롭게 transaction 생성하고
Application에서 exception 난 경우에만 rollback 명령어를 실행하는 구조였던 것이였습니다.
마치며...
아주 흥미로웠던 주제와 파고들었던 시간에 비하면 생각보다 허무하고 당연했던(?) 결말이였지만
이번 기회를 통해 Transaction 에 대한 환상이 깨졌으며 용 빼는 재주는 없구나 내부 동작과정을 이해하는 것이 장애 상황에 대응 하기위해 얼마나 중요한지 새삼스럽게 깨닫게 되는 시간이였습니다.
긴 글 읽어주셔서 감사드립니다!
'Java' 카테고리의 다른 글
[GC] 나야 메모리, 근데 이제 누수를 곁들인 (2) | 2024.10.22 |
---|---|
[Base64] 빼앗긴 Parameter 찾습니다. (3) | 2024.10.11 |
[Transactional] 거래가 있었는데요.. 없었습니다 (2) | 2024.08.17 |
[Java의 동시성] - Java에서 동시성 문제를 해결하는 방법 (1) | 2023.05.07 |
[동시성 문제] - 동시성 문제란 무엇이며 어떻게 해결해야할까? (0) | 2023.05.01 |