Member, Account 라는 2개의 데이터 소스를 사용하는 프로젝트가 있다. 더티체킹을 통해 Account 라는 엔티티의 데이터를 업데이트 하려고 한다. 코드를 보자.
@Transactional
public void update() {
var account = accountRepository.findByMemberId(member.getId())
.orElseThrow();
account.updateAccount(UPDATED_ACCOUNT);
}
더티체킹을 통해 업데이트를 하는 전형적인 로직이다. 왜 updateAccount
메서드 종료 후에 실제 DB 에 반영이 되지 않았을까?
@EnableJpaRepositories(
basePackages = "com.spring.boilerplate.repository.member",
entityManagerFactoryRef = "memberEntityManagerFactory",
transactionManagerRef = "memberTransactionManager"
)
public class MemberDataSourceConfig {
@Primary
@Bean
public PlatformTransactionManager memberTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(memberEntityManagerFactory().getObject());
return transactionManager;
}
...
@EnableJpaRepositories(
basePackages = "com.spring.boilerplate.repository.account",
entityManagerFactoryRef = "accountEntityManagerFactory",
transactionManagerRef = "accountTransactionManager"
)
@RequiredArgsConstructor
public class AccountDataSourceConfig {
@Bean
public PlatformTransactionManager accountTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(accountEntityManagerFactory().getObject());
return transactionManager;
}
...
Member, Account 두개의 데이터소스가 있을 때, Member 트랜잭션 매니저가 Primary Bean 으로 등록을 해놓았네..?!
@Transactional
public void update(String email) {
Member member = memberRepository.findByEmail(email)
.orElseThrow();
member.updateEmail(OUTER_CHANGED_EMAIL);
}
@Transactional
에 아무런 설정이 없는 경우, Member 트랜잭션 매니저가 Primary Bean 으로 등록되어있기 때문에 Member 더티체킹은 정상 동작하는 것을 알 수 있다.
@Transactional("accountTransactionManager")
public void updateWithAccountEntityManager() {
var account = accountRepository.findByMemberId(member.getId())
.orElseThrow();
account.updateAccount(UPDATED_ACCOUNT);
}
@Transactional
에 어떤 트랜잭션 매니저를 주입할 것인지 명시해주면 된다. Account 데이터 소스에 있는 엔티티를 수정하고 싶기 때문에 accountTransactionManager 를 주입해주었다. 결과는 정상 동작한다!
@Transactional
public void updateWithAccountEntityManager() {
var account = accountRepository.findByMemberId(member.getId())
.orElseThrow();
account.updateAccount(UPDATED_ACCOUNT);
accountRepository.save(account);
}
accountRepository 는 EnableJpaRepositories
설정을 통해 account 트랜잭션 매니저가 관리하도록 설정하였으므로, save() 쿼리 메서드 내에 있는 @Transactional
은 Account 트랜잭션 매니저가 관리하게 된다. 결과적으로 save() 쿼리 메서드를 사용한다면, 업데이트가 가능하다.
@EnableJpaRepositories(
basePackages = "com.spring.boilerplate.repository.account",
entityManagerFactoryRef = "accountEntityManagerFactory",
transactionManagerRef = "accountTransactionManager"
)
@RequiredArgsConstructor
public class AccountDataSourceConfig {
A 메서드 내에서 B 메서드를 호출한다.
A 는 Member 를 업데이트하고, B 메서드에 파라미터로 업데이트한 Member 를 넘긴다. B 는 받은 Member 를 업데이트하고 Accoun 를 조회하여 업데이트한다.
-- A 몌서드
@Transactional
public Member update(String email) {
Member member = memberRepository.findByEmail(email)
.orElseThrow();
member.updateEmail(OUTER_CHANGED_EMAIL);
accountService.update(member); // B 메서드 호출
return member;
}
-- B 메서드
@Transactional
public void update(Member member) {
member.updateEmail(INNER_UPDATED_EMAIL);
var account = accountRepository.findByMemberId(member.getId())
.orElseThrow();
account.updateAccount(UPDATED_ACCOUNT);
}
@DisplayName("member 엔티티 매니저가 수행하는 내부 트랜잭션에서 account 더티체킹이 동작하지 않는다.")
@Test
void dirtyCheckingWithPrimaryEntityManagerTest() {
memberService.update(NEW_EMAIL);
var account = accountRepository.findById(accountId).get();
assertThat(account.getAccount()).isEqualTo(NEW_ACCOUNT);
}
-- A 몌서드
@Transactional
public Member updateWithSpecificEntityManager(String email) {
Member member = memberRepository.findByEmail(email)
.orElseThrow();
member.updateEmail(OUTER_CHANGED_EMAIL);
accountService.updateWithAccountEntityManager(member); // B 메서드 호출
return member;
}
-- B 메서드
@Transactional("accountTransactionManager")
public void updateWithAccountEntityManager(Member member) {
member.updateEmail(INNER_UPDATED_EMAIL);
var account = accountRepository.findByMemberId(member.getId())
.orElseThrow();
account.updateAccount(UPDATED_ACCOUNT);
}
@DisplayName("account 엔티티 매니저가 수행하는 내부 트랜잭션에서 account 더티체킹이 동작한다.")
@Test
void dirtyCheckingWithAccountEntityManagerTest() {
memberService.updateWithSpecificEntityManager(NEW_EMAIL);
var account = accountRepository.findById(accountId).get();
assertThat(account.getAccount()).isEqualTo(UPDATED_ACCOUNT);
}
-- A 몌서드
@Transactional
public Member update(String email) {
Member member = memberRepository.findByEmail(email)
.orElseThrow();
member.updateEmail(OUTER_CHANGED_EMAIL);
accountService.update(member); // B 메서드 호출
return member;
}
-- B 메서드
@Transactional
public void update(Member member) {
member.updateEmail(INNER_UPDATED_EMAIL);
var account = accountRepository.findByMemberId(member.getId())
.orElseThrow();
account.updateAccount(UPDATED_ACCOUNT);
}
@DisplayName("member 엔티티 매니저가 수행하는 내부 트랜잭션에서 member 더티체킹이 동작한다.")
@Test
void dirtyCheckingWithPrimaryEntityManagerTest() {
memberService.update(NEW_EMAIL);
var member = memberRepository.findById(memberId).get();
assertThat(member.getEmail()).isEqualTo(INNER_UPDATED_EMAIL);
}
account 더티체킹 동작하지 않는 코드와 동일하다. Member 는 더티체킹이 동작하는 것을 알 수 있다.
-- A 몌서드
@Transactional
public Member updateWithSpecificEntityManager(String email) {
Member member = memberRepository.findByEmail(email)
.orElseThrow();
member.updateEmail(OUTER_CHANGED_EMAIL);
accountService.updateWithAccountEntityManager(member); // B 메서드 호출
return member;
}
-- B 메서드
@Transactional("accountTransactionManager")
public void updateWithAccountEntityManager(Member member) {
member.updateEmail(INNER_UPDATED_EMAIL);
var account = accountRepository.findByMemberId(member.getId())
.orElseThrow();
account.updateAccount(UPDATED_ACCOUNT);
}
@DisplayName("account 엔티티 매니저가 수행하는 내부 트랜잭션에서 member 더티체킹이 동작한다.")
@Test
void dirtyCheckingWithAccountEntityManagerTest() {
memberService.updateWithSpecificEntityManager(NEW_EMAIL);
var member = memberRepository.findById(memberId).get();
assertThat(member.getEmail()).isEqualTo(INNER_UPDATED_EMAIL);
}
account 더티체킹 동작하는 코드와 동일하다. Member 는 여기서도 더티체킹이 동작하는 것을 알 수 있다 😬
-- A 메서드
@Transactional
public Member updateWithSpecificEntityManagerAndThrow(String email) {
Member member = memberRepository.findByEmail(email)
.orElseThrow();
member.updateEmail(OUTER_CHANGED_EMAIL);
accountService.updateWithAccountEntityManager(member); // B 메서드
throw new RuntimeException();
}
-- B 메서드
@Transactional("accountTransactionManager")
public void updateWithAccountEntityManager(Member member) {
member.updateEmail(INNER_UPDATED_EMAIL);
var account = accountRepository.findByMemberId(member.getId())
.orElseThrow();
account.updateAccount(UPDATED_ACCOUNT);
}
@DisplayName("내부 트랜잭션은 account 엔티티 매니저가 수행해서, member 만 롤백된다.")
@Test
void updateWithSpecificEntityManagerAndThrow() {
assertThatExceptionOfType(Exception.class)
.isThrownBy(() -> memberService.updateWithSpecificEntityManagerAndThrow(NEW_EMAIL));
var member = memberRepository.findById(memberId).orElseThrow();
var account = accountRepository.findById(accountId).orElseThrow();
assertThat(member.getEmail()).isEqualTo(NEW_EMAIL);
assertThat(account.getAccount()).isEqualTo(UPDATED_ACCOUNT);
}
그렇다... B 메서드는 accountTransactionManager 가 관리하기 때문에 롤백되지 않는다...
하지만 데이터 정합성을 위해서 둘다 롤백이 필요한 경우가 더 많을 것이다. 이 경우에는 어떻게 해야할까?
@Transactional
public Member updateAndInnerThrow(String email) {
Member member = memberRepository.findByEmail(email)
.orElseThrow();
member.updateEmail(OUTER_CHANGED_EMAIL);
accountService.updateAndThrow(member);
TransactionSynchronizationManager.getResourceMap(); // 🔥🔥🔥
return member;
}
🔥🔥🔥 디버깅해보자. 프록시, 히카리 데이터 소스 설정 등을 볼 수 있다. 알려주신 규현님께 감사합니다🙇♀️
@Transactional
public Member updateAndInnerThrow(String email) {
Member member = memberRepository.findByEmail(email)
.orElseThrow();
member.updateEmail(OUTER_CHANGED_EMAIL);
accountService.updateAndThrow(member); // B 메서드
return member;
}
@DisplayName("내부에서 예외가 발생하여, 둘다 롤백된다.")
@Test
void updateAndInnerThrow() {
assertThatExceptionOfType(Exception.class)
.isThrownBy(() -> memberService.updateAndInnerThrow(NEW_EMAIL));
var member = memberRepository.findById(memberId).orElseThrow();
var account = accountRepository.findById(accountId).orElseThrow();
assertThat(member.getEmail()).isEqualTo(NEW_EMAIL);
assertThat(account.getAccount()).isEqualTo(NEW_ACCOUNT);
}
B 메서드가 제일 나중에 수행될 수 있도록 하여, A 메서드에서 예외 발생 시에는 B 가 수행되지 못하도록하여 데이터 정합성을 처리한다. B 에서 수행시에 발생한 예외는 A 에서도 롤백이 되므로 데이터 정합성을 맞출 수 있다.
위 경우는 B 메서드 반환값이 void 로, B 에서 처리한 데이터를 A 에서 사용하지 않는다. 하지만 B 에서 처리한 데이터로 나머지 A 로직에서 추가로 처리해야 하는 경우도 있다. 이런 경우 글로벌 트랜잭션(분산 트랜잭션)을 사용할 수 있는데... 다음 블로그 포스트에서 알아보자.
오늘도 배워 갑니닷!! 👍👍👍