본문 바로가기
Project/깃터디 (gitudy)

[깃터디/Alarm] 스터디장 가입승인/거부 알림(fcm) api 구현

by J-rain 2024. 5. 16.

💡 .java를 클릭시 관련 커밋으로 이동💡

 

 회고 

비동기 처리의 테스트 도중 아래와 같이 오류가 발생했다.

Wanted but not invoked:
fcmService bean.sendMessageSingleDevice(
    <any com.example.backend.study.api.event.FcmSingleTokenRequest>
);
-> at com.example.backend.study.api.event.service.FcmService.sendMessageSingleDevice(FcmService.java:37)
Actually, there were zero interactions with this mock.

Wanted but not invoked:
fcmService bean.sendMessageSingleDevice(
    <any com.example.backend.study.api.event.FcmSingleTokenRequest>
);
-> at com.example.backend.study.api.event.service.FcmService.sendMessageSingleDevice(FcmService.java:37)
Actually, there were zero interactions with this mock.

	at com.example.backend.study.api.event.service.FcmService.sendMessageSingleDevice(FcmService.java:37)
	at com.example.backend.domain.define.study.info.listener.StudyEventListenerTest.apply_test(StudyEventListenerTest.java:62)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

오류 발생

@Async로 표시된 @EventListener가 발생시킨 이벤트에 대해 비동기적으로 처리되는 동안, 테스트 코드가 fcmService.sendMessageSingleDevice 메서드의 호출을 검증하기를 기다리지 않고 너무 빠르게 진행되어서 발생하는 내용... 즉, 이벤트 리스너가 비동기적으로 실행되기 때문에, 실제 메소드 호출이 verify 검증 코드 실행 시점 이전에 이루어지지 않았을 수 있는것!

 

CountDownLatch를 사용하여 비동기 작업의 완료를 기다린 후 검증을 수행 방법으로 해결했다. 이 방법은 비동기 작업이 몇 번 실행될지 알고 있을 때 유용한 방법인데 스레드가 다른 스레드들의 작업이 모두 완료 될 때까지 기다리도록 해주었다.

@SuppressWarnings("NonAsciiCharacters")
public class StudyEventListenerTest extends TestConfig {
    @Autowired
    private FcmTokenRepository fcmTokenRepository;

    @Autowired
    private StudyEventListener studyEventListener;

    @MockBean
    private FcmService fcmService;

    @MockBean
    private FirebaseMessaging firebaseMessaging;

    @AfterEach()
    void tearDown() {
        fcmTokenRepository.deleteAll();
    }

    @Test
    @DisplayName("스터디 가입 신청 리스너 테스트")
    void apply_test() throws Exception {
        // given
        CountDownLatch latch = new CountDownLatch(1);
        Long leaderId = 1L;

        ApplyMemberEvent applyMemberEvent = StudyEventFixture.generateApplyMemberEvent(leaderId);

        FcmToken fcmToken = FcmFixture.generateDefaultFcmToken(leaderId);

        when(fcmService.findFcmTokenByIdOrThrowException(leaderId)).thenReturn(fcmToken);

        // when
        studyEventListener.applyMemberListener(applyMemberEvent);

        // then
        latch.await(2, TimeUnit.SECONDS);
        verify(fcmService).sendMessageSingleDevice(any(FcmSingleTokenRequest.class)); // sendMessageSingleDevice 호출 검증
    }

    @Test
    @DisplayName("스터디 가입 신청 승인/거부 리스너 테스트")
    void apply_approve_refuse_test() throws Exception {
        // given
        CountDownLatch latch = new CountDownLatch(1);
        Long applyUserId = 1L;

        ApplyApproveRefuseMemberEvent applyApproveRefuseMemberEvent = StudyEventFixture.generateApplyApproveRefuseMemberEvent(applyUserId);

        FcmToken fcmToken = FcmFixture.generateDefaultFcmToken(applyUserId);
        
        when(fcmService.findFcmTokenByIdOrThrowException(applyUserId)).thenReturn(fcmToken);

        // when
        studyEventListener.applyApproveRefuseMemberListener(applyApproveRefuseMemberEvent);

        // then
        latch.await(2, TimeUnit.SECONDS);
        verify(fcmService).sendMessageSingleDevice(any(FcmSingleTokenRequest.class)); // sendMessageSingleDevice 호출 검증
    }
}

이벤트 리스너가 처리하는 평균시간이 750ms 정도여서 넉넉하게 2초정도 await() 메소드를 호출한 스레드를 대기시킨다.

 

 

ApplyApproveRefuseMemberListener.java

@Component
@RequiredArgsConstructor
@Slf4j
public class ApplyApproveRefuseMemberListener {

    private final FcmService fcmService;

    @Async
    @EventListener
    public void applyApproveRefuseMemberListener(ApplyApproveRefuseMemberEvent event) throws FirebaseMessagingException {

        FcmToken fcmToken = fcmService.findFcmTokenByIdOrThrowException(event.getApplyUserId());

        String title;
        String message;

        if (event.isApprove()) {
            title = "[" + event.getStudyTopic() + "] 스터디 신청";
            message = String.format("축하합니다! '%s'님 가입이 승인되었습니다!", event.getName());

        } else {
            title = "[" + event.getStudyTopic() + "] 스터디 신청";
            message = String.format("안타깝게도 '%s'님은 가입이 거절되었습니다.", event.getName());
        }

        fcmService.sendMessageSingleDevice(FcmSingleTokenRequest.builder()
                .token(fcmToken.getFcmToken())
                .title(title)
                .message(message)
                .build());
    }
}

Fcm도메인에서 리스너 관리 ⇒ 각각의 행동에 맞게 클래스명 수정

 

 

ApplyApproveRefuseMemberEvent.java

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApplyApproveRefuseMemberEvent {

    private boolean approve;   // 승인 여부

    private Long applyUserId;  // 가입신청자 Id

    private String studyTopic;  // 스터디 제목

    private String name;      // 가입신청자 이름
}

댓글