이것저것 알아봤으니 이제 프로젝트 코드에 알림 트랜잭션 분리를 적용해 보고자 한다.
챌린지 인증 임박 알림 트랜잭션 분리 테스트 작성
@DisplayName("알림 이벤트에 예외가 발생해도 새로운 사이클은 저장된다.")
@Test
void cycleCreate_pushEventException() throws InterruptedException {
// given
LocalDateTime now = LocalDateTime.now();
willThrow(new RuntimeException("알림 로직에 예상치 못한 예외 발생!"))
.given(pushEventListener).handle(any(PushEvent.class));
// when
Long cycleId = cycleService.create(
new TokenPayload(조조그린_ID),
new CycleRequest(now, 스모디_방문하기_ID)
);
// then
Optional<Cycle> cycle = cycleService.findById(cycleId);
List<PushNotification> notifications = pushNotificationRepository.findAll();
assertAll(
() -> assertThat(cycle).isPresent(),
() -> assertThat(notifications).isEmpty()
);
}
알림 이벤트 처리에 @TransactinalEventListenr와 전파 속성 REQUIRES_NEW로 처리
이전 포스팅에서 검증한 대로 테스트는 다 통과하였다.
@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handle(PushEvent event) {
pushStrategies.get(event.getPushCase())
.push(event.getEntity());
}
알림 로직에서 병목이 발생하면 어떻게 될까?
@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handle(PushEvent event) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
pushStrategies.get(event.getPushCase())
.push(event.getEntity());
}
다른 트랜잭션이라도 한 스레드가 동기적으로 실행하는 일이기 때문에 사이클도 저장되는 데 한참 걸린다.
비동기 처리를 해야할 필요를 느꼈다.
비동기 처리
@SpringBootApplication
@EnableScheduling
@EnableJpaAuditing
@EnableAsync
public class SmodyApplication {
@Async
@TransactionalEventListener
public void handle(PushEvent event) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
pushStrategies.get(event.getPushCase())
.push(event.getEntity());
}
이제 아예 다른 스레드에서 실행되기 때문에 트랜잭션 분리도 자연스럽게 될 것이다.
테스트도 순식간에 도는 것을 확인했지만 알림 처리 스레드가 Thread.sleep() 때문에 늦게 동작하면서 알림이 저장되지 않아 알림 저장 테스트들이 실패했다.
비동기 처리가 되는 것을 확인했으니Thread.sleep() 코드를 제거하고 다시 돌려봤다.
그럼에도 잘 되던 테스트들이 실패했다.
비동기 테스트는 어떻게?
디버그를 찍어 보니 알림 스레드가 알림을 저장하기도 전에 main 스레드가 알림이 저장되었는지 검증을 해버려서 테스트가 실패하였다.
참고:https://stackoverflow.com/questions/42438862/junit-testing-a-spring-async-void-service-method
위 링크의 방법대로 모든 활성 스레드를 종료시킨 후 검증을 다시 해보았다.
사이클과 알림 모두 저장하는 테스트가 비동기 처리에서도 통과하는 것을 확인할 수 있다.
비동기로 인해 JPA 지연 로딩을 하지 못하는 경우 발생
위 테스트는 통과했지만 통과하지 않는 테스트가 생겼다.
LazyInitializationException은 JPA 관련 예외로 프록시 객체를 지연 로딩하지 못할 때 발생한다.
@Override
@Transactional
public void push(Object entity) {
Cycle cycle = (Cycle) entity;
deleteInCompleteNotificationIfSamePathIdPresent(cycle);
if (cycle.isSuccess()) {
return;
}
pushNotificationService.register(buildNotification(cycle));
}
//... 나중에 cycle.getChallenge().getName()을 호출할 때 예외 발생!
EventListenr는 이벤트를 수신하고 알맞은 알림 전략 클래스의 push() 메서드를 호출한다.
이 때 관련 엔티티를 전달받는데 이 메서드가 호출되는 건 저 엔티티를 생성한 스레드와 다른 스레드다.
즉 저 엔티티는 영속성 컨텍스트의 관리를 받지 못하는 준영속 상태인 것이다.
Cycle cycle = cycleService.search(((Cycle) entity).getId());
그래서 CycleService를 의존하여 다시 조회하도록 하니 테스트가 통과하였다.
결론
어노테이션 단 2개로 비동기 처리를 할 수 있었다.
알림 이벤트를 발생시키는 곳과 이벤트를 처리하는 곳의 스레드가 달라지면서 테스트하기가 조금 힘들어지거나 jpa 관련 이슈가 있었지만 다행히 금방 해결할 수 있었다.
'우아한테크코스 > 프로젝트-SMODY' 카테고리의 다른 글
운영 서버에서 예외 발생 시 자동 깃헙 이슈 생성 (0) | 2022.10.12 |
---|---|
[Spring] 동시성과 예외, 그리고 트랜잭션 (0) | 2022.10.01 |
이벤트 트랜잭션 분리와 테스트에서의 @Transactional 문제 (0) | 2022.08.30 |
결합은 낮추고 전략은 다양하게, 알림 기능 적용기 (0) | 2022.08.15 |
[JPA] 조회 쿼리 N+1 문제 해결 (0) | 2022.07.10 |