영호
[Spring] @TransactionalEventListener 에서 CUD 가 안되는 이유 본문
들어가면서,
우테코 프로젝트에서 @TransactionalEventListener 를 사용할 때 READ 는 되는데 CUD 쿼리가 발생하지 않는 문제를 겪었다. 그래서 이에 대한 원인에 대해 공부한 과정을 정리 해볼 예정이다.
트랜잭션 생성 과정과 커밋 과정
REQUIRED, REQUIES_NEW 옵션의 트랜잭션 생성 과정
각각에 대한 자세한 내용은 링크에 있습니다.
간단하게 요약하자면, 아래와 같은 흐름으로 진행됩니다.
- 트랜잭션 생성 시 스레드 로컬에 트랜잭션 관련 자원이 있으면 기존 트랜잭션이 있다고 판단.
- REQUIRED 옵션은 새로운 트랜잭션을 논리 트랜잭션으로 생성
- REQUIRES_NEW 는 스레드 로컬에 자원이 있어도 새로운 물리 트랜잭션으로 생성
- 이후 커밋 시 물리 트랜잭션이라면 flush() 가 호출
- 이후 스레드 로컬 자원 정리
- 커밋 시 논리 트랜잭션이면 flush() 호출 X
@TransactionalEventListener + @Transaction
모두 기본 옵션인 AFTER_COMMIT, REQUIRED 를 사용한다고 가정하겠습니다.
코드는 아래와 같습니다.
Publisher (이벤트 발행자)
@Transactional
@RequiredArgsConstructor
@Service
public class Publisher {
private final ApplicationEventPublisher publisher;
private final EventEntityRepo eventEntityRepo;
public void publish() {
eventEntityRepo.save(new EventEntity());
publisher.publishEvent(new MyEvent());
}
}
Listener (이벤트 구독자)
@RequiredArgsConstructor
@Component
public class Listner {
private static final Logger log = LoggerFactory.getLogger(Listner.class);
private final EventEntityRepo eventEntityRepo;
private final EntityManager em;
@TransactionalEventListener
public void listen(MyEvent myEvent) {
EventEntity save = eventEntityRepo.save(new EventEntity());
}
}
Publisher 로직이 커밋되어 이제 Listener 로직이 수행 되는 상황입니다. 이 때, Listener의 이벤트 구독 메서드에서 repository.save() 를 호출합니다.
위 코드의 흐름은 아래와 같습니다.
- Publisher 측의 메서드가 실행되면서 트랜잭션을 획득한다.
- Publisher 측 repository.save() 가 호출된다.
- 이벤트를 발행한다.
- Publisher 측 트랜잭션이 커밋된다.
- Listener의 메서드가 실행된다.
- Listener 측의 repository.save() 가 호출된다.
- 이 때, 트랜잭션을 얻고 커밋까지 진행합니다.
- 하지만 논리트랜잭션이라 커밋되지 않습니다.
- Listener 측 메서드가 종료된다.
- Publisher 측 트랜잭션에 관한 스레드로컬 자원이 정리된다.
여기서 주목할 점은 4, 5 번 사이입니다. 아마 Listener가 없었으면 4번 이후 8번 작업이 바로 실행됐을겁니다. 그러나 @TransactionalEventListener 가 존재하므로, 5번 작업이 수행됩니다.
5번 작업이 수행되는 상황을 도식화 하면 아래와 같습니다.
Publisher 트랜잭션이 커밋 되었더라도 아직 스레드 로컬에 자원이 남아 있기 때문에 Listener 측에서 얻는 트랜잭션은 논리 트랜잭션 입니다. 물리 트랜잭션이 커밋되어야 Listener 측의 쿼리가 flush() 가 되는데, 이미 물리 트랜잭션은 커밋된 상태라 Listener 측의 쿼리는 발생하지 않는 것입니다.
해결방법
그렇다면 해결 방법은 Listener 측에서 새로운 트랜잭션을 얻게 하면 됩니다.
@Async
트랜잭션을 얻을 때 기존 트랜잭션이 있다고 판단하는 첫 번째 근거는 스레드 로컬의 자원 유무입니다. 그러면 새로운 스레드에서 이벤트 구독 메서드가 실행되도록 하면 됩니다.
REQUIRED_NEW
@Transactional 의 옵션을 REQUIRED_NEW 로 주는 방법도 있습니다. REQUIRED_NEW 는 기존 스레드 로컬에 다른 트랜잭션 자원이 있어도 새로운 물리 트랜잭션을 반환해줍니다.
그러나 이는 커넥션 풀의 커넥션을 하나 더 쓰는 것이기 때문에 주의해야 합니다.
혹시 잘못된 부분 있으면 언제든 댓글 달아주세요~
'Spring' 카테고리의 다른 글
[Spring] TaskScheduler 를 활용해 만료된 인증코드 제거하기 (0) | 2024.03.12 |
---|---|
스탬프 중복 적립 개선기 (0) | 2024.02.23 |
[Spring] 외부 API 와 비즈니스 로직 분리하기 (0) | 2023.10.15 |
[Spring] @Transactional 의 REQUIRED, REQUIRES_NEW 트랜잭션 생성 과정 (1) | 2023.10.14 |
[Spring] @Transactional 물리 트랜잭션과 논리 트랜잭션 커밋 동작 차이 (2) | 2023.10.14 |