Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] 입장 10분 전 푸시 알림 전송 기능 구현 (#483) #484

Closed
wants to merge 47 commits into from
Closed
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
cbcad44
feat: 입장 알림 전송 기능 구현
xxeol2 Oct 1, 2023
21f86ff
refactor: FCMClient 인터페이스화
xxeol2 Oct 2, 2023
4c17ab6
refactor: 스케쥴링 중복 실행시 409 Conflict
xxeol2 Oct 2, 2023
2e4530d
refactor: entryAlert 비관적락 적용
xxeol2 Oct 2, 2023
4a77dd7
refactor: EntryAlertEventListener에서 EntryAlertRepository 참조 제거
xxeol2 Oct 2, 2023
690f90f
refactor: FCM Aycnc Config 정의
xxeol2 Oct 2, 2023
323c6d9
feat: flyway script 작성
xxeol2 Oct 2, 2023
1651604
feat: MockFcmClient 생성
xxeol2 Oct 2, 2023
3b308b5
refactor: FCMNotificationEventListener Async 빈 지정
xxeol2 Oct 2, 2023
97daba8
feat: SpringBoot 실행시 initSchedule
xxeol2 Oct 2, 2023
aa4e530
refactor: Async ThreadPool 하나만 활용하도록 수정
xxeol2 Oct 2, 2023
44ced57
test: FcmClientImplTet 작성
xxeol2 Oct 2, 2023
bd7451b
feat: MemberFcmFixture 정의
xxeol2 Oct 2, 2023
221cac0
refactor: findAllTokenByMemberIdIn 메서드 활용
xxeol2 Oct 2, 2023
a083e3c
style: 패키지 네이밍 수정 (entry_alert -> entryalert)
xxeol2 Oct 2, 2023
6aebcd5
refactor: 500건만 전송하도록 하는 로직 FcmClientImpl로 이동
xxeol2 Oct 4, 2023
da70c4d
refactor: fcmToken String으로 추출
xxeol2 Oct 4, 2023
4c20e30
refactor: ApplicationReadyEvent Listener 최상단으로 이동
xxeol2 Oct 4, 2023
03588d1
refactor: taskScheduler에 람다식으로 넘겨주도록 리팩토링
xxeol2 Oct 4, 2023
64759af
refactor: EntryAlert에 AlertStatus 추가
xxeol2 Oct 4, 2023
a29ae79
refactor: FCM 전송시 데드락 문제 해결
xxeol2 Oct 4, 2023
49c15e6
refactor: AsyncBatch 템플릿 콜백 패턴 적용
xxeol2 Oct 4, 2023
e253317
refactor: Async ThreadPool설정 클래스 위치 이동
xxeol2 Oct 4, 2023
78e5820
refactor: EntryAlertResultHandler 네이밍 수정
xxeol2 Oct 4, 2023
7b4a210
refactor: fcmExecutor QueueCapacity 기본값으로 수정
xxeol2 Oct 4, 2023
fff1325
feat: flyway 추가
xxeol2 Oct 4, 2023
6995f68
test: LocalDateTime 타임존 UTC로 수정
xxeol2 Oct 4, 2023
639e228
Merge remote-tracking branch 'origin/dev' into feat/#483
xxeol2 Oct 5, 2023
ac669fc
fix: EntryAlertServiceTest Mock 객체 수정
xxeol2 Oct 5, 2023
dd9d56f
refactor: test LocalDateTime 수정
xxeol2 Oct 5, 2023
828935c
Merge remote-tracking branch 'origin/dev' into feat/#483
xxeol2 Oct 5, 2023
26fed9f
test: EntryAlertRepositoryTest 작성
xxeol2 Oct 5, 2023
4577784
refactor: JPQL 파라미터 @Param 어노테이션 추가
xxeol2 Oct 5, 2023
44c583b
refactor: 빈 토큰 검사 메서드 네이밍 수정
xxeol2 Oct 5, 2023
a42051a
refactor: SENT/FAILED 상태 삭제
xxeol2 Oct 5, 2023
7d872a9
refcator: EntryAlert 스케쥴링 추가 로깅 자세하게 수정
xxeol2 Oct 5, 2023
310cd14
remove: 불필요한 유틸 클래스 삭제
xxeol2 Oct 5, 2023
ce2adf1
refactor: Test 의존성 추가
xxeol2 Oct 5, 2023
b081b35
refactor: FCmClient로 빈 토큰이 넘어오면 early return
xxeol2 Oct 5, 2023
40f9359
feat: EntryAlert 알림 전송 시작 로깅
xxeol2 Oct 5, 2023
8965022
fix: MockFcmClient Profile 수정
xxeol2 Oct 5, 2023
e374d66
refactor: FcmClientImpl Executor -> TaskExecutor로 수정
xxeol2 Oct 5, 2023
d1f1792
refactor: 접근제어자 변경 public -> private
xxeol2 Oct 5, 2023
6e06a21
refactor: 롬복 적용
xxeol2 Oct 5, 2023
94a3d55
refactor: EntryAlert request상태로 변경 메서드 네이밍 수정
xxeol2 Oct 5, 2023
e45fde3
refactor: sendEntryAlert에서 예외를 던지지 않는 방향으로 수정
xxeol2 Oct 5, 2023
e2b1eaa
refactor: graceful shutdown 설정
xxeol2 Oct 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.festago.common.exception;

public class ConflictException extends FestaGoException {

public ConflictException(ErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public enum ErrorCode {
DUPLICATE_STUDENT_EMAIL("이미 인증된 이메일입니다."),
TICKET_CANNOT_RESERVE_STAGE_START("공연의 시작 시간 이후로 예매할 수 없습니다."),
INVALID_STUDENT_VERIFICATION_CODE("올바르지 않은 학생 인증 코드입니다."),
INVALID_ENTRY_ALERT_TIME("올바르지 않은 입장 알림 시간입니다"),
NOT_PENDING_ALERT("전송 대기중인 알림이 아닙니다."),
NOT_REQUESTED_ALERT("전송 요청중인 알림이 아닙니다."),


// 401
EXPIRED_AUTH_TOKEN("만료된 로그인 토큰입니다."),
Expand All @@ -45,6 +49,10 @@ public enum ErrorCode {
FESTIVAL_NOT_FOUND("존재하지 않는 축제입니다."),
TICKET_NOT_FOUND("존재하지 않는 티켓입니다."),
SCHOOL_NOT_FOUND("존재하지 않는 학교입니다."),
ENTRY_ALERT_NOT_FOUND("존재하지 않는 입장 알림입니다."),

// 409
ALREADY_ALERT("이미 입장 알림이 전송되었습니다."),

// 500
INTERNAL_SERVER_ERROR("서버 내부에 문제가 발생했습니다."),
Expand Down
10 changes: 10 additions & 0 deletions backend/src/main/java/com/festago/config/SchedulingConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.festago.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
public class SchedulingConfig {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.festago.entryalert.application;

import com.festago.entryalert.dto.EntryAlertResponse;
import com.festago.ticket.dto.event.TicketCreateEvent;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
public class EntryAlertEventListener {

private static final Logger log = LoggerFactory.getLogger(EntryAlertEventListener.class);

private final EntryAlertService entryAlertService;
private final TaskScheduler taskScheduler;
private final Clock clock;

public EntryAlertEventListener(EntryAlertService entryAlertService, TaskScheduler taskScheduler, Clock clock) {
this.entryAlertService = entryAlertService;
this.taskScheduler = taskScheduler;
this.clock = clock;
}
xxeol2 marked this conversation as resolved.
Show resolved Hide resolved

@EventListener(ApplicationReadyEvent.class)
public void initEntryAlertSchedule() {
List<EntryAlertResponse> entryAlerts = entryAlertService.findAllPending();
entryAlerts.forEach(this::addSchedule);
}

private void addSchedule(EntryAlertResponse entryAlert) {
Long entryAlertId = entryAlert.id();
Instant alertTime = toInstant(entryAlert.alertTime());
log.info("EntryAlert 스케쥴링 추가. entryAlertId: {}, alertTime: {}", entryAlertId, entryAlert.alertTime());
taskScheduler.schedule(() -> entryAlertService.sendEntryAlert(entryAlertId), alertTime);
}

private Instant toInstant(LocalDateTime localDateTime) {
return localDateTime.atZone(clock.getZone()).toInstant();
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async
public void addEntryAlertSchedule(TicketCreateEvent event) {
EntryAlertResponse entryAlert = entryAlertService.create(event.stageId(), event.entryTime());
addSchedule(entryAlert);
}
Comment on lines +45 to +50
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entryAlertServicecreate() 메서드는 DB에 저장하는 로직이라, 굳이 @Async로 등록을 해야할 필요가 있을까 싶네요.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entryAlertService의 create() 메서드가 @Transactional 메서드라서 @Async로 등록해줬습니다~!
(@Async로 등록하지 않으면 기존 트랜잭션이 이미 커밋된 상태라서 insert문이 동작하지 않아요!
propagation=REQUIRES_NEW로 하면 데드락 문제가 일어나고..)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.festago.entryalert.application;

import com.festago.common.exception.ConflictException;
import com.festago.common.exception.ErrorCode;
import com.festago.entryalert.domain.AlertStatus;
import com.festago.entryalert.domain.EntryAlert;
import com.festago.entryalert.dto.EntryAlertResponse;
import com.festago.entryalert.repository.EntryAlertRepository;
import com.festago.fcm.application.FcmClient;
import com.festago.fcm.domain.FCMChannel;
import com.festago.fcm.dto.FcmPayload;
import com.festago.fcm.repository.MemberFCMRepository;
import com.festago.ticketing.repository.MemberTicketRepository;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class EntryAlertService {

private final EntryAlertRepository entryAlertRepository;
private final MemberTicketRepository memberTicketRepository;
private final MemberFCMRepository memberFCMRepository;
private final FcmClient fcmClient;
private final TaskExecutor taskExecutor;

@Transactional(readOnly = true)
public List<EntryAlertResponse> findAllPending() {
return entryAlertRepository.findAllByStatus(AlertStatus.PENDING)
.stream()
.map(alert -> new EntryAlertResponse(alert.getId(), alert.findAlertTime()))
.toList();
}

public EntryAlertResponse create(Long stageId, LocalDateTime entryTime) {
EntryAlert entryAlert = entryAlertRepository.save(new EntryAlert(stageId, entryTime));
return new EntryAlertResponse(entryAlert.getId(), entryAlert.findAlertTime());
}

@Async
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 메서드에 @Async가 필요한 이유는 EntryAlertEventListeneraddSchedule() 메서드에서 작업을 등록할 때 스케쥴러가 해당 메서드를 비동기로 실행하기 위함인가요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네넵 정확합니다!

ScheduleThreadpool이 아닌 AsyncThreadPool에서 해당 메서드가 실행되도록 유도했습니다.
(ScheduleThreadpool corePoolSize: 1 / AsyncThreadPool corePoolSize: 8)

ScheduleThread는 그저 작업들이 대기하다가 특정 시각이 되면 메서드를 실행시키는 역할만 수행하도록 했습니다!

public void sendEntryAlert(Long id) {
EntryAlert entryAlert = entryAlertRepository.findByIdAndStatusForUpdate(id, AlertStatus.PENDING)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

X락을 거느라, 서버가 여러 개 생겼을 때 다른 서버들은 여기서 대기를 하겠네요.
해당 방법은 추후에 락을 걸더라도 락이 걸려있으면 바로 빠져나오게 할 방법이 필요하겠네요.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

대기를 하는 것을 의도한게 맞습니다!
여러 서버에서 동시에 실행되면 한 대의 서버에서만 실행됨을 보장해야해 X-lock을 걸어줬습니다!
X-lock 범위를 최소화하기 위해 실제 FCM 전송 로직은 @Async로 분리해줬습니다!

Copy link
Collaborator

@carsago carsago Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redis를 이용한 분산락, mysql을 통한 분산락(https://techblog.woowahan.com/2631/) 등으로 추후에 변경을 모색해볼 수 있지만
나중에 저희가 정말 필요하다가 판단될 때 도입하는게 좋을 것 같습니당. (현재는 이렇게 하는게 가장 간단하니까요)

.orElseThrow(() -> new ConflictException(ErrorCode.ALREADY_ALERT));
List<String> tokens = findFcmTokens(entryAlert);
taskExecutor.execute(() -> fcmClient.sendAll(tokens, FCMChannel.ENTRY_ALERT, FcmPayload.entryAlert()));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EntryAlertFcmClient 클래스가 사라졌네요!
TaskExecutorexecute 메소드를 호출해서 비동기로 실행하는 것보다 다시 EntryAlertFcmClient를 추가해서 비동기로 실행하는 것은 어떤가요?
그렇게 한다면 다음과 같이 실패한 요청을 로그로 남길 수 있을 것 같아요.

@Component
@RequiredArgsConstructor
@Slf4j
public class EntryAlertFcmClient {

    private final FcmClient fcmClient;

    @Async
    public void sendAll(Long entryAlertId, List<String> tokens, FcmPayload payload) {
        boolean isSuccess = fcmClient.sendAll(tokens, FCMChannel.ENTRY_ALERT, payload);
        if (!isSuccess) {
            log.warn("무대 입장 알림 전송이 실패했습니다. entryAlertId: {}", entryAlertId);
        }
    }
}

Copy link
Member Author

@xxeol2 xxeol2 Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sendAll에서 500건 이상이면 내부적으로 Async로 동작해서(500건씩 잘라서 별도의 스레드에서 전송) isSuccess 여부를 알아오기가 쉽지 않아요 ... ㅠㅠ
그걸 알기 위해선 위의 CompletableFuture 도입이 다시 필요해집니다 🥲

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EntryAlertFcmClient를 굳이 클래스로 분리하지 않은 이유는
fcmClient.sendAll(tokens, FCMChannel.ENTRY_ALERT, payload); 메서드를 호출하는 역할 뿐이어서
taskExecutor.execute 하는게 더 간단하고 Async로 돌아가는걸 메서드단에서 파악하기 쉬워서 분리하지않았는데
이 부분은 분리하는게 좋을까요?!

Copy link
Member Author

@xxeol2 xxeol2 Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아니면 500건 이상일때 비동기로 보내지않고 한 스레드에서 순차적으로 보내는 방법도 존재합니다..!!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

500건 이상일때 순차적으로 보내는 것도 좋겠네요. 😂

entryAlert.request();
}

private List<String> findFcmTokens(EntryAlert entryAlert) {
List<Long> memberIds = memberTicketRepository.findAllOwnerIdByStageIdAndEntryTime(
entryAlert.getStageId(), entryAlert.getEntryTime());
return memberFCMRepository.findAllTokenByMemberIdIn(memberIds);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.festago.entryalert.domain;

public enum AlertStatus {

PENDING,
REQUESTED,
;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.festago.entryalert.domain;

import com.festago.common.domain.BaseTimeEntity;
import com.festago.common.exception.BadRequestException;
import com.festago.common.exception.ErrorCode;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Entity
public class EntryAlert extends BaseTimeEntity {

private static final int ENTRY_ALERT_MINUTES_BEFORE = 10;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotNull
private Long stageId;

@NotNull
private LocalDateTime entryTime;

@NotNull
@Enumerated(value = EnumType.STRING)
private AlertStatus status = AlertStatus.PENDING;

protected EntryAlert() {
}
xxeol2 marked this conversation as resolved.
Show resolved Hide resolved

public EntryAlert(Long stageId, LocalDateTime entryTime) {
this(null, stageId, entryTime);
}

public EntryAlert(Long id, Long stageId, LocalDateTime entryTime) {
this.id = id;
this.stageId = stageId;
this.entryTime = entryTime;
}

public static EntryAlert create(Long stageId, LocalDateTime entryTime, LocalDateTime currentTime) {
if (currentTime.isAfter(entryTime.minusMinutes(ENTRY_ALERT_MINUTES_BEFORE))) {
throw new BadRequestException(ErrorCode.INVALID_ENTRY_ALERT_TIME);
}
return new EntryAlert(stageId, entryTime);
}

public void request() {
validateNotPending();
this.status = AlertStatus.REQUESTED;
}
xxeol2 marked this conversation as resolved.
Show resolved Hide resolved

private void validateNotPending() {
if (status != AlertStatus.PENDING) {
throw new BadRequestException(ErrorCode.NOT_PENDING_ALERT);
}
}

public LocalDateTime findAlertTime() {
return entryTime.minusMinutes(ENTRY_ALERT_MINUTES_BEFORE);
}

public Long getId() {
return id;
}

public Long getStageId() {
return stageId;
}

public LocalDateTime getEntryTime() {
return entryTime;
}

public AlertStatus getStatus() {
return status;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.festago.entryalert.dto;

import java.time.LocalDateTime;

public record EntryAlertResponse(
Long id,
LocalDateTime alertTime
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.festago.entryalert.repository;

import com.festago.entryalert.domain.AlertStatus;
import com.festago.entryalert.domain.EntryAlert;
import jakarta.persistence.LockModeType;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface EntryAlertRepository extends JpaRepository<EntryAlert, Long> {

@Query("SELECT ea FROM EntryAlert ea WHERE ea.status = :status")
List<EntryAlert> findAllByStatus(@Param("status") AlertStatus status);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT ea FROM EntryAlert ea WHERE ea.id = :id and ea.status = :status")
Optional<EntryAlert> findByIdAndStatusForUpdate(@Param("id") Long id, @Param("status") AlertStatus status);
}
Original file line number Diff line number Diff line change
@@ -1,90 +1,29 @@
package com.festago.fcm.application;

import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.InternalServerException;
import com.festago.entry.dto.event.EntryProcessEvent;
import com.festago.fcm.domain.FCMChannel;
import com.festago.fcm.dto.MemberFCMResponse;
import com.google.firebase.messaging.AndroidConfig;
import com.google.firebase.messaging.AndroidNotification;
import com.google.firebase.messaging.BatchResponse;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.SendResponse;
import com.festago.fcm.dto.FcmPayload;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
@Profile({"dev", "prod"})
public class FCMNotificationEventListener {

private static final Logger log = LoggerFactory.getLogger(FCMNotificationEventListener.class);

private final FirebaseMessaging firebaseMessaging;
private final FcmClient fcmClient;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FcmClient 를 추상화해준 이유가 추후에 IosFcmClient, webFcmClient를 고려한 것이 맞을까용?

Copy link
Member Author

@xxeol2 xxeol2 Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확장을 위한 추상화보다는 DIP를 위한 추상화입니다!!
FirebaseMessaging은 외부 라이브러리라고 판단해 infrastructure 레이어에 두기 위해서 추상화를 진행했습니다..!

private final MemberFCMService memberFCMService;

public FCMNotificationEventListener(FirebaseMessaging firebaseMessaging, MemberFCMService memberFCMService) {
this.firebaseMessaging = firebaseMessaging;
public FCMNotificationEventListener(FcmClient fcmClient, MemberFCMService memberFCMService) {
this.fcmClient = fcmClient;
this.memberFCMService = memberFCMService;
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async
public void sendFcmNotification(EntryProcessEvent event) {
List<Message> messages = createMessages(getMemberFCMToken(event.memberId()), FCMChannel.NOT_DEFINED.name());
try {
BatchResponse batchResponse = firebaseMessaging.sendAll(messages);
checkAllSuccess(batchResponse, event.memberId());
} catch (FirebaseMessagingException e) {
log.error("fail send FCM message", e);
throw new InternalServerException(ErrorCode.FAIL_SEND_FCM_MESSAGE);
}
}

private List<String> getMemberFCMToken(Long memberId) {
return memberFCMService.findMemberFCM(memberId).memberFCMs().stream()
.map(MemberFCMResponse::fcmToken)
.toList();
}

private List<Message> createMessages(List<String> tokens, String channelId) {
return tokens.stream()
.map(token -> createMessage(token, channelId))
.toList();
}

private Message createMessage(String token, String channelId) {
return Message.builder()
.setAndroidConfig(createAndroidConfig(channelId))
.setToken(token)
.build();
}

private AndroidConfig createAndroidConfig(String channelId) {
return AndroidConfig.builder()
.setNotification(createAndroidNotification(channelId))
.build();
}

private AndroidNotification createAndroidNotification(String channelId) {
return AndroidNotification.builder()
.setChannelId(channelId)
.build();
}

private void checkAllSuccess(BatchResponse batchResponse, Long memberId) {
List<SendResponse> failSend = batchResponse.getResponses().stream()
.filter(sendResponse -> !sendResponse.isSuccessful())
.toList();

log.warn("member {} 에 대한 다음 요청들이 실패했습니다. {}", memberId, failSend);
throw new InternalServerException(ErrorCode.FAIL_SEND_FCM_MESSAGE);
List<String> tokens = memberFCMService.findMemberFCMTokens(event.memberId());
fcmClient.sendAll(tokens, FCMChannel.ENTRY_PROCESS, FcmPayload.entryProcess());
}
}
10 changes: 10 additions & 0 deletions backend/src/main/java/com/festago/fcm/application/FcmClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.festago.fcm.application;

import com.festago.fcm.domain.FCMChannel;
import com.festago.fcm.dto.FcmPayload;
import java.util.List;

public interface FcmClient {

void sendAll(List<String> tokens, FCMChannel channel, FcmPayload fcmPayload);
Comment on lines +8 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void sendAll(List<String> tokens, FCMChannel channel, FcmPayload fcmPayload);
@Async
void sendAll(List<String> tokens, FCMChannel channel, FcmPayload fcmPayload);

이렇게 달면 안돼나요? overflow

Copy link
Member Author

@xxeol2 xxeol2 Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

놀랍게도 sendAll은 Async 메서드가 아닙니다..!
그 이유는 QR 상태 변경용 FCM에서는 이미 비동기 메서드에서 해당 메서드를 호출하기때문에
해당 메서드에 @Async 달면 두 번 Async 처리가 되더라구요!
그래도 큰 문제는 없지만, 굳이 두 번 Async를 탈 필요는 없어서 저렇게 진행했습니다

image

Copy link
Member

@BGuga BGuga Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러면

@Async
    public void sendEntryAlert(Long id) {
        EntryAlert entryAlert = entryAlertRepository.findByIdAndStatusForUpdate(id, AlertStatus.PENDING)
            .orElseThrow(() -> new ConflictException(ErrorCode.ALREADY_ALERT));
        List<String> tokens = findFcmTokens(entryAlert);
        log.info("EntryAlert 전송 시작 / entryAlertId: {} / to {} devices", id, tokens.size());
        taskExecutor.execute(() -> fcmClient.sendAll(tokens, FCMChannel.ENTRY_ALERT, FcmPayload.entryAlert()));  ✅
        entryAlert.request();
    }

마찬가지로 요기도

@Async
    public void sendEntryAlert(Long id) {
        EntryAlert entryAlert = entryAlertRepository.findByIdAndStatusForUpdate(id, AlertStatus.PENDING)
            .orElseThrow(() -> new ConflictException(ErrorCode.ALREADY_ALERT));
        List<String> tokens = findFcmTokens(entryAlert);
        log.info("EntryAlert 전송 시작 / entryAlertId: {} / to {} devices", id, tokens.size());
        fcmClient.sendAll(tokens, FCMChannel.ENTRY_ALERT, FcmPayload.entryAlert()); ✅
        entryAlert.request();
    }

이렇게 하면 안되는 것인가용?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그래도 되긴 하는데... entryAlertRepository.findByIdAndStatusForUpdate() 메서드가 락을 걸기도 하고, 트랜잭션 범위안에 있어서 이건 비동기로 재호출을 한 번 더 해야될 것 같네요

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아아 x 락 범위가 있었네요 굿굿

}
Loading
Loading