우아한테크코스/프로젝트-SMODY

[Spring] 알림 기능 비동기 처리하기

더즈 2022. 9. 17. 18:38

알림 적용기 1편

알림 적용기 2편

이것저것 알아봤으니 이제 프로젝트 코드에 알림 트랜잭션 분리를 적용해 보고자 한다.

챌린지 인증 임박 알림 트랜잭션 분리 테스트 작성

@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 관련 이슈가 있었지만 다행히 금방 해결할 수 있었다.