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

[깃터디/Alarm] 스터디 가입 신청 알림(fcm) api 구현

by J-rain 2024. 5. 16.

 

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

 

 회고 

비동기처리 공부

Spring Boot에서 비동기 처리는 멀티스레딩 환경에서 비동기적으로 실행되는 작업을 처리하는 것으로, 동기적인 방식과 비교해 처리 속도와 성능을 개선할 수 있다.

비동기 처리 작업이란 멀티스레드를 사용하여 작업을 분리하고, 작업이 끝날 때까지 대기하지 않고 다른 작업을 처리할 수 있다.

@Async어노테이션은 Spring이 제공하는 기능으로, 해당 어노테이션이 붙은 메서드를 비동기 처리로 실행할 수 있도록 해준다. 이를 사용하면 메서드가 실행되는 동안 다른 작업을 수행할 수 있으며, 작업의 완료 여부를 확인할 수 있다.

    @Async
    @EventListener
    public void applyMemberListener(ApplyMemberEvent event) throws FirebaseMessagingException {

        FcmToken fcmToken = fcmTokenRepository.findById(event.getStudyLeaderId()).orElseThrow(() -> {
            log.warn(">>>> {} : {} <<<<", event.getStudyLeaderId(), ExceptionMessage.FCM_DEVICE_NOT_FOUND);
            return new EventException(ExceptionMessage.FCM_DEVICE_NOT_FOUND);
        });

        fcmService.sendMessageSingleDevice(FcmSingleTokenRequest.builder()
                .token(fcmToken.getFcmToken())
                .title(event.getTitle())
                .message(event.getMessage())
                .build());
                
        for (int i = 0; i < 30; i++) {
            try {
                Thread.sleep(500);
                log.info("[AsyncMethod]" + "-" + i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } 
    }
    
비동기로 처리를 하는지 확인하기위해 반복문을 넣어주었다.

 

@EnableAsync를 작성했을 때

 

task-1라는 이름으로 비동기 메소드가 호출될 때 별도의 스레드 이름이 나오는 것을 확인할 수 있다.

 

@EnableAsync를 작성하지 않았을 때

nio-8080-exec-7 는 스레드가 웹 요청을 처리하는 스레드 풀의 일부라는 것을 볼 수 있다. nio-8080-exec- 접두사는 톰캣(Tomcat)이나 스프링 부트(Spring Boot)와 같은 내장 서버에서 사용하는 NIO(Non-Blocking I/O) 기반의 스레드 풀에서 자주 볼 수 있는 명명 규칙 이다. 따라서 로그의 스레드 이름이 비동기 처리를 위한 별도의 스레드가 아니라 웹 서버의 작업 처리 스레드를 나타내고 있다.

 

사용해야 하는 이유는?

1. 외부 API 호출⇒ Fcm알림

외부 API를 호출할때 해당 API의 응답을 기다리는 동안 서버의 자원이 블로킹될 수 있다. 이를 방지하기 위해 비동기 처리를 사용하여 API 호출 결과를 기다리지 않고, 다른 작업을 수행할 수 있도록 해주는 것!

비동기 처리 했을 때 장점

1. 높은 응답성

동기적인 방식으로 작업을 처리할 때, 작업이 끝날 때까지 다른 요청을 처리할 수 없다. 하지만 비동기 처리는 작업이 완료되기를 기다리지 않기 때문에 요청에 대한 응답 시간을 줄일 수 있다. 예를 들어 외부 서비스와 통신할 때 I/O 작업이 많은 경우 작업이 끝날 때 까지 기다리는 대신 다른 요청을 처리하며 시간을 절약할 수 있다.

2. 자원 효율성

동기적인 방식으로 작업을 처리할 때는 스레드를 많이 생성해야 하기 때문에 시스템 자원을 많이 사용한다.

비동기처리는 작업이 끝날 때까지 스레드를 차지하지 않기 때문에 자원을 효율적으로 사용할 수 있다. 또한 스레드가 많아짐에 따라 처리량을 늘리는 것이 가능해 시스템이 확장될 때도 확장성을 유지할 수 있다.

즉, 비동기 처리를 이용하여 자원을 효율적으로 분배하여 응답시간을 단축하는 것이 주 목적

 

 

StudyEventListener.java

@Component
@RequiredArgsConstructor
@Slf4j
public class StudyEventListener {

    private final FcmService fcmService;
    private final FcmTokenRepository fcmTokenRepository;

    @Async
    @EventListener
    public void applyMemberListener(ApplyMemberEvent event) throws FirebaseMessagingException {

        FcmToken fcmToken = fcmTokenRepository.findById(event.getStudyLeaderId()).orElseThrow(() -> {
            log.warn(">>>> {} : {} <<<<", event.getStudyLeaderId(), ExceptionMessage.FCM_DEVICE_NOT_FOUND);
            return new EventException(ExceptionMessage.FCM_DEVICE_NOT_FOUND);
        });

        fcmService.sendMessageSingleDevice(FcmSingleTokenRequest.builder()
                .token(fcmToken.getFcmToken())
                .title("[" + event.getStudyTopic() + "] 스터디 신청")
                .message(event.getName() + "님이 스터디를 신청했습니다.\\n" + "프로필과 메시지를 확인 후, 수락해주세요!")
                .build());
    }
}

이벤트 처리 리스너

 

 

ApplyMemberEvent.java

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

    private Long studyLeaderId;  // 스터디 리더 Id

    private String studyTopic;  // 스터디 제목

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

}

이벤트 객체

 

 

StudyMemberService.java

eventPublisher.publishEvent(ApplyMemberEvent.builder()
                        .studyLeaderId(leader.getId())
                        .studyTopic(studyInfo.getTopic())
                        .name(user.getName())
                        .build());

eventPublisher 추가 ⇒ 서비스단에서는 이벤트를 발행만해주고 어떻게 이벤트를 다루는지 몰라야함

 


테스트

 

FcmFixture.java

 public static FcmToken generateDefaultFcmToken(Long userId) {
        return FcmToken.builder()
                .userId(userId)
                .fcmToken("token")
                .build();
    }

 

 

StudyEventFixture.java

  // 가입신청 이벤트 fixture
    public static ApplyMemberEvent generateApplyMemberEvent(Long studyLeaderId) {
        return ApplyMemberEvent.builder()
                .studyLeaderId(studyLeaderId)
                .studyTopic("스터디제목")
                .name("가입자이름")
                .build();
    }

이벤트를 받기위한 DTO Fixture이다.

 

 

StudyEventListenerTest.java

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

    @InjectMocks
    private StudyEventListener studyEventListener;

    @Mock
    private FcmService fcmService;

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

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

        ApplyMemberEvent applyMemberEvent = StudyEventFixture.generateApplyMemberEvent(leaderId);

        FcmToken fcmToken = FcmFixture.generateDefaultFcmToken(leaderId);

        when(fcmTokenRepository.findById(any(Long.class))).thenReturn(Optional.of(fcmToken));

        // when
        studyEventListener.applyMemberListener(applyMemberEvent);

        // then
        verify(fcmService).sendMessageSingleDevice(any(FcmSingleTokenRequest.class)); // sendMessageSingleDevice 호출 검증
    }
}

댓글