프로젝트에서 repository.save() 메서드를 사용했음에도 불구하고 데이터가 커밋되지 않는 문제를 겪은 적이 있습니다. 이 글에서는 문제의 원인과 해결 방법을 공유합니다.
트랜잭션 처리 방식
문제의 원인을 파악하기 위해 하나의 쓰레드에서 두 개의 메서드를 사용하며, 각각의 트랜잭션이 어떻게 작동하는지 세 가지 케이스로 살펴보겠습니다.
1. 첫 번째 케이스: 동일 트랜잭션
두 메서드(A 트랜잭션, B 트랜잭션)가 동일한 트랜잭션에 포함되어 있습니다. 즉, A와 B가 모두 성공해야 트랜잭션이 커밋됩니다. 만약 B에서 예외가 발생하면, A에서의 작업까지 모두 롤백됩니다.
|-----------(A 트랜잭션)-----------|
|-----------(B 트랜잭션)-----------|
2. 두 번째 케이스: 독립적인 트랜잭션
A 트랜잭션과 B 트랜잭션이 서로 독립적입니다. B 트랜잭션에서 예외가 발생하더라도 A 트랜잭션은 영향을 받지 않으며, A는 정상적으로 커밋됩니다.
|-----------(A 트랜잭션)-----------|
|-----------(B 트랜잭션)-----------|
3. 세 번째 케이스: 포함된 트랜잭션
A 트랜잭션이 B 트랜잭션을 포함하지만, 둘은 하나의 트랜잭션으로 간주됩니다. 따라서 B에서 예외가 발생하면 A 트랜잭션도 롤백됩니다.
|---------------------(A 트랜잭션)---------------------|
|-----------(B 트랜잭션)-----------|
문제 발생 코드
@Transactional
open fun transferMoney(senderId: Long, recipientId: Long, sendAmount: BigDecimal) {
...
if (RequestTracker.getRequestTimes(senderAccount.id!!).size > blockAttemptCount) {
accountService.save(senderAccount)
throw IllegalStateException("Account ${senderAccount.id} has been blocked due to abnormal activity. (Max request count exceeded)")
}
}
fun save(senderAccount: Account) {
accountRepository.save(senderAccount)
}
위 코드에서 save() 메서드가 실행되었음에도 불구하고 데이터가 커밋되지 않는 문제가 발생했습니다. 이는 트랜잭션이 하나로 묶여 있기 때문에, 예외가 발생하면 전체 트랜잭션이 롤백되기 때문입니다.
JpaRepository의 save() 메서드가 기본적으로 @Transactional로 처리되지만, 상위 메서드(transferMoney)에서 발생한 예외로 인해 하위 메서드에서의 작업까지 롤백됩니다.
이 상황은 세 번째 케이스와 동일한 트랜잭션 동작 방식입니다. B 트랜잭션이 실패하면 A 트랜잭션도 롤백됩니다.
문제 해결 방법
문제를 해결하기 위해서는 A 트랜잭션과 B 트랜잭션을 독립적으로 처리하거나, 예외를 던지기 전 미리 커밋을 해야합니다.
즉, 각각의 트랜잭션이 별도로 커밋되도록 트랜잭션 전파 방식을 명시적으로 나누어 처리하거나 EntityManager를 통해 강제로 커밋하는 방식이 있습니다.
여기서는 트랜잭션을 분리해서 처리하겠습니다.
# Another Service class
@Transactional
open fun transferMoney(senderId: Long, recipientId: Long, sendAmount: BigDecimal) {
...
if (RequestTracker.getRequestTimes(senderAccount.id!!).size > blockAttemptCount) {
...
// 강제 커밋이므로 주의 필요
accountService.saveAccountWithNewTransaction(senderAccount)
throw IllegalStateException("Account ${senderAccount.id} has been blocked due to abnormal activity. (Max request count exceeded)")
}
}
# Service class
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun saveAccountWithNewTransaction(senderAccount: Account) {
accountRepository.save(senderAccount)
}
해결된 트랜잭션 동작
위 코드에서 Propagation.REQUIRES_NEW 옵션을 사용하여 A 트랜잭션과 B 트랜잭션을 각각 독립적으로 처리하게 되었습니다. 이로써 B 트랜잭션이 실패하더라도 A 트랜잭션은 영향을 받지 않고 정상적으로 커밋됩니다.
|---------------------(A 트랜잭션)-------------------롤백|
|--------(새로운 B 트랜잭션)------커밋|
이 방식은 트랜잭션이 서로 독립적으로 작동하므로, 예외가 발생하더라도 특정 트랜잭션만 롤백되고 나머지는 정상적으로 처리됩니다.
그러나 B트랜잭션이 커밋되기 전까지 의도하지 않은 데이터가 B 트랜잭션에서 커밋이 될 수 있으니 주의해야합니다.
결론
트랜잭션 간 독립성을 보장하기 위해서는 Propagation.REQUIRES_NEW 옵션을 사용하여 각각의 트랜잭션이 독립적으로 커밋되도록 처리하는 것이 중요합니다. 이 방식은 예외가 발생하더라도 특정 트랜잭션만 롤백되고, 나머지는 정상적으로 커밋되도록 합니다. 이를 통해 트랜잭션 간 영향을 최소화하고, 필요한 데이터가 의도대로 커밋되도록 보장할 수 있습니다.
이와 같은 방식으로 트랜잭션 동작을 개선할 수 있으며, 프로젝트에서 유사한 문제를 경험할 때 Propagation 옵션을 활용하여 적절한 트랜잭션 처리를 할 수 있습니다.
'Spring Data' 카테고리의 다른 글
JPA와 Hibernate의 차이점은 무엇인가요? (0) | 2023.09.02 |
---|---|
ORM이란 무엇인가요? (0) | 2023.09.02 |
JPA가 무엇인가요? (0) | 2023.09.02 |
Kafka Streams로 실시간 데이터 분석, Kafka Connect로 데이터 통합 (0) | 2023.08.08 |
Kafka 아키텍처와 ZooKeeper의 역할: 고가용성 분산 시스템의 설계와 관리 (0) | 2023.08.07 |