여기 간단한 서비스 클래스가 있다.
@Service class UserService( private val userRepository: UserRepository, private val teamService: TeamService, ) { @Transactional(propagation = Propagation.REQUIRED) fun saveUser(user: User) { teamService.saveTeam(user.team) userRepository.save(user) } } @Service class TeamService( private val teamRepository: TeamRepository, ) { @Transactional(propagation = Propagation.REQUIRES_NEW) fun saveTeam(team: Team) { teamRepository.save(team) } }
UserService의 saveUser()는 TeamService 클래스에 위치한 saveTeam()을 호출한다.
그리고 saveUser()와 saveTeam()은 각각 다른 트랜잭션 전파 정책을 사용한다.
보통 saveTeam()은 saveUser()의 트랜잭션 전파로 인해 같은 트랜잭션 정책을 사용하지만,
예시에서는 saveTeam()은 saveUser()가 호출했다 하더라도 다른 트랜잭션 정책을 사용하고 싶은 상황이다.
실제로 다른 트랜잭션 정책을 사용하는지 확인해보자.
편의를 위해 디버그 모드를 사용했다.

(사진에 숫자 표시)
1. saveUser()을 호출한다.
2. @Transactional -> 트랜잭션 기능 제공을 위해 프록시 객체를 호출한다.
3. TransactionInterceptor 및 TransactionAspectSupport 클래스에서 트랜잭션 기능을 제공한다.
- dataSource를 가져온다.
- 실제 saveUser() 메서드를 호출한다.
- 성공or실패 시, 커밋or롤백을 수행한다.
4. 실제 saveUser()를 호출한다. (Breakpoint)
-> 즉, 트랜잭션 기능이 적용된 프록시 객체에서 실제 saveUser() 메서드를 호출하는 것을 알 수 있다.
saveTeam()은 어떨까? 코드를 계속 진행해보자.

saveUser()와 동일한 과정으로 새로운 트랜잭션 프록시를 적용하는 것을 확인할 수 있다.
만약, saveTeam()에 별도로 트랜잭션 정책을 적용하지 않는다면?

saveUser()에만 (당연히) 트랜잭션 프록시를 적용하고,
saveTeam()은 트랜잭션 전파로 인해 바로 메서드 call을 진행한다.
여기까지는 익숙한 개념일 수 있다.
그러나, @Transactional을 사용할 때 주의할 점으로, 같은 클래스 내 메서드 호출이 있다.
살짝 다른 예시를 보자.
@Service class UserService( private val userRepository: UserRepository, private val teamRepository: TeamRepository, ) { @Transactional(propagation = Propagation.REQUIRED) fun saveUser(user: User) { saveTeam(user.team) userRepository.save(user) } @Transactional(propagation = Propagation.REQUIRES_NEW) fun saveTeam(team: Team) { teamRepository.save(team) } }
이번엔 TeamService가 아닌 UserService 클래스 내부에서 saveTeam()을 호출한다.
이 때, 트랜잭션 전파 정책도 서로 다른 것을 사용한다.
디버그 모드로 돌려보자.

saveUser()까지는 (당연히) 문제 없이 트랜잭션 프록시를 적용한다.
그렇다면 saveTeam()은?

TeamService를 호출했을 때와 달리 같은 클래스 내 내부 메서드 호출은 트랜잭션 프록시를 적용하지 않는 것을 볼 수 있다.
이렇게 되면 saveTeam()의 트랜잭션 정책은 무시하고, saveUser() 트랜잭션이 그대로 전파되게 된다.
why?
이유는 간단하다. 트랜잭션 프록시를 포함해, 프록시 객체는 클래스 단위로 만들게 된다.
UserService -> TeamService 구조에서는,
UserService 프록시 -> saveUser() -> TeamService 프록시 -> saveTeam() 구조로 호출할 수 있기 때문에,
각 클래스마다 트랜잭션 프록시를 적용할 수 있는 반면,
UserService 내부 호출의 경우,
UserService 프록시 -> saveUser() -> saveTeam() 구조로 호출하기 때문에, 최초 트랜잭션 정책인 saveUser()의 정책만 적용한다.
@Transactional 포함 프록시 객체를 사용할 때 주의해야 할 부분인 것 같다.
+) 만약 saveTeam()을 private으로 변경하는 경우, 어차피 외부에서 호출될 일이 없고, 같은 클래스 내 @Transactional 메서드의 정책이 그대로 전파될 수 밖에 없기 때문에, 이렇게 컴파일 타임에 오류를 던지고 있다.

'java > spring' 카테고리의 다른 글
[Spring] ObjectMapper -> Jackson2ObjectMapperBuilder vs 생성자 (2) | 2025.02.17 |
---|---|
[Spring] jackson 역직렬화, 필드가 1개일 때 HttpMessageNotReadableException (4) | 2025.02.09 |
[Spring] Pointcut 유형에 따라 Proxy 생성 방식이 달라진다? (CGLIB or JDK Proxy) (0) | 2024.11.02 |
[Spring] junit test에서 lombok 사용하는 방법 (0) | 2024.10.16 |
[Spring] ControllerAdvice - 예외 처리 (2) | 2023.07.16 |