Spring Boot. JPA Repository insert multi table under the same Transaction.
공부할 겸 JPA를 한번 사용해 보려고 한다.
환경.
java 17
spring boot 3.1.1
목표.
한 트랜젝션에서 여러개의 테이블에 insert나 update 작업이 필요하면 어떡하지?
문제.
JPA의 레파지토리를 보면 CrudRepository 인터페이스부터 save()를 제공한다.
이 save 메소드는 SimpleJpaRepository에서 overriding해서 구현되어진다.
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
...
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
...
}
문제는 save()에 @Transactional이 붙어있어 서비스단에서 save()를 사용하면 바로 db에 적용된다는 것이다.
해결.
해결 방법은 간단하다. 늘 그랬듯 필요한 곳에서 트랜젝션을 걸어주면 된다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository repository;
@Transactional
public User signUp(SignUp signUp){
SignUp signUp1 = new SignUp("abc");
SignUp signUp2 = new SignUp("abc");
SignUp result1 = repository.save(signUp1);
SignUp result2 = repository.save(signUp2);
return null;
}
}
형태에서 알 수 있듯 @Transactional은 AOP이기 때문에 메소드가 끝나면 트랜젝션에 정의된 로직을 실행한다.
- 대표적으로 커밋 이나 롤백
- AOP 이기 때문에 private 메소드에서는 작동 안 할 것이다.
여기까지 오면 한가지 의문이 생긴다.
SimpleJpaRepository의 save()에도 @Transactional이 붙어 있다는 것이다. 그러면 save()가 끝났을 때 바로 커밋이 되어야 하는 것 아닌가? => 아니다!!!
하나 더 생각해야한다. 지금 처럼 @Transactional 안에 @Transactional이 붙어있는 메소드를 호출하면 어떻게 될까?
// 대략 이런 형태
@Transactional
method(){
@Transactional
save();
}
가장 바깥에 있는 @Transactional만 실행한다.
== 처음 만난 @Transactional만 실행한다.
== 안쪽의 @Transactional은 무시한다.
@Transactional을 따라가보자.
save()를 호출하는 메소드에 @Transactional을 붙인 것과 안붙인 것을 비교했다.
똑같이 진행 되다가 처음 차이가 발생한 곳은 TransactionAspectSupport.java 에서 TransactionInfo가 생성되고 값이 셋팅 될 때이다. 트랜젝션 실행에 영향을 가장 크게 미친다고 생각되는 것은 trasactionStatus.newTransaction과 oldTransactionInfo이다.
@Transactional 붙였을 때 | @Transactional 안붙였을 때 |
![]() |
![]() |
마치 한 쌍처럼 움직이는데 oldTransactionInfo에 값이 있으면 newTransaction은 false이고 반대의 경우는 true이다.
java 코드
@Transactional1 <- oldTransaction:null
@Transactional2 <- oldTransaction:Transactional1
@Transactional3 <- oldTransaction:Transactional2
txInfo
TransactionInfo3
> newTransaction: false
> oldTransactionInfo: TransactionInfo2
> newTransaction: false
> oldTransactionInfo: TransactionInfo1
> newTransaction: true
> oldTransactionInfo: null
method 실행 순서에 따라 스택에 쌓이고 마지막에 가장 바깥에 있는 @Transactional만 실행하는 구조인 듯 하다.
커밋은 마지막 실행전에 newTransaction를 확인한다.
AbstractPlatformTransactionManager.java
processCommit(){
...
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction commit");
}
unexpectedRollback = status.isGlobalRollbackOnly();
doCommit(status);
}
...
}
결론.
DB 작업을 한 트랜젝션 아래에서 하고 싶으면 그냥 @Transactional을 쓰자. 단 AOP이기 때문에 private 메소드는 안된다.
-----------------------------------
추가적인 질문이 생길 수 있다. 다음에 시간이 나면 정리해보겠다. (우선 키워드만 남긴다)
- jpa에서 flush는 무슨 일을 하나?
- 영속성 컨텍스트(persistence context), flush - AOP는 왜 private에서 작동 안하나?
- AOP proxy