업무 중 실제 겪은 일이지만, 보안상 다른 예시로 대체하겠다. CRUD의 내용은 같으니 참고하면 좋을 것 같다.
경험
회원 가입이 됐을 경우 쿠폰을 발급되고 DB에 저장되는 기능을 개발하고 있었다.
Service Layer가 길어지는게 보기 싫었던 나는 Event Listner를 사용하기로 결정했다.
@Transactional
public void signUp(User user) {
// 1. 회원 정보 저장
memberRepository.save(user);
// 2. 회원가입 완료 이벤트 발행
eventPublisher.publishEvent(new UserSignedUpEvent(user.getId()));
}
회원가입이 되면 이벤트를 발생시키는 코드
@EventListener
public void handleUserSignedUpEvent(UserSignedUpEvent event) {
// 이벤트가 발행되면 쿠폰을 발급
User newUser = memberRepository.findById(event.getUserId())
.orElseThrow(() -> new EntityNotFoundException("회원 못찾음"));
couponService.issueCoupon(newUser);
}
이벤트가 발행되면 쿠폰을 발급시키는 EventListner 코드이다.
여기서 문제점이 생겼다. 분명 회원가입이 완료되면, 그 회원에게 쿠폰을 발행시키도록 하는 완벽한 설계라 생각했다.
하지만 EventListener에서 memberRepository.findById(...)를 실행할 때, 회원을 찾지 못해서 EntityNotFoundException이 터지는 현상이 계속 발생했다.
이유를 모르겠으니 EntityNoFoundException이 터질 때마다 내 속도 터져갔다.
DB 문제일까? 로직 문제일까? 여러 고민을 해보고, Debug로 하나하나 메세드를 짚어가며 찾아봤다. 답이 안 나왔다.
CS를 공부해보니 AOP가 문제가 아닐까? 라는 생각이 들어 @Transactional을 공부해보니 알 수 있었다.
원인
@EventListener는 기본적으로 @Transactional 메서드와 같은 트랜잭션 안에서 동작한다.
signUp 메서드가 아직 커밋(Commit)되기 전에 handleUserSignedUpEvent가 호출된 것이다.
즉, 회원가입이 다 완료가 안 됐는데 EventListner를 호출해 쿠폰을 발급한 것이다.
DB에 회원 정보가 최종적으로 반영이 안 됐는데, EventListner가 "DB 가서 회원 찾아오렴" 이러는데 어떻게 찾을까
정신이 번쩍 들었다.
해
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleUserSignedUpEvent(UserSignedUpEvent event) {
...
}
코드를 이와 같이 바꿨다
phase = TransactionPahse.AFTER_COMMIT 옵션은 singUp 메서드의 트랜잭션이 성공적으로 커밋된 후에야 쿠폰 발급 로직이 실행되도록 하는 것이다.
이와 같이 Transaction에 옵션을 걸어 관리한다면 보다 더 편리하고 AOP에 걸리지 않게 개발할 수 있다.
결론 : Transcation 알고 쓰자. 남발하지 말자.
'Back-End' 카테고리의 다른 글
| LDAP (3) | 2025.09.02 |
|---|---|
| [Spring boot] 중앙 집중식 예외 처리 (2) | 2025.08.14 |
| 동기와 비동기 (1) | 2025.05.30 |
| [Redis] 기본 개념 (1) | 2025.05.26 |
| [Java] JPA 영속성 (1) | 2025.05.23 |