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] feat: 새로운 FCM 등록 기능 추가 (#998) #1002

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
@@ -1,6 +1,8 @@
package com.festago.fcm.application;

import com.festago.auth.dto.event.MemberDeletedEvent;
import com.festago.fcm.application.command.MemberFCMCommandService;
import com.festago.member.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
Expand All @@ -9,10 +11,11 @@
@RequiredArgsConstructor
public class FCMMemberDeleteEventListener {

private final MemberFCMService memberFCMService;
private final MemberFCMCommandService memberFCMService;

@EventListener
public void memberDeleteEventHandler(MemberDeletedEvent event) {
memberFCMService.deleteAllMemberFCM(event.member().getId());
Member member = event.member();
memberFCMService.deleteAllMemberFCM(member.getId());
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.festago.fcm.application;

import com.festago.entry.dto.event.EntryProcessEvent;
import com.festago.fcm.application.command.MemberFCMCommandService;
import com.festago.fcm.domain.FCMChannel;
import com.festago.fcm.domain.MemberFCM;
import com.festago.fcm.repository.MemberFCMRepository;
import com.google.firebase.messaging.AndroidConfig;
import com.google.firebase.messaging.AndroidNotification;
import com.google.firebase.messaging.BatchResponse;
Expand All @@ -10,25 +13,23 @@
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.SendResponse;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;

// TODO 비즈니스 로직 새로운 Service 클래스 생성 뒤 이관할 것
@Component
@Profile("prod | dev")
@Slf4j
@RequiredArgsConstructor
public class FCMNotificationEventListener {

private final FirebaseMessaging firebaseMessaging;
private final MemberFCMService memberFCMService;

public FCMNotificationEventListener(FirebaseMessaging firebaseMessaging, MemberFCMService memberFCMService) {
this.firebaseMessaging = firebaseMessaging;
this.memberFCMService = memberFCMService;
}
private final MemberFCMRepository memberFCMRepository;

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async
Expand All @@ -44,7 +45,9 @@ public void sendFcmNotification(EntryProcessEvent event) {
}

private List<String> getMemberFCMTokens(Long memberId) {
return memberFCMService.findAllMemberFCMTokens(memberId);
return memberFCMRepository.findAllByMemberId(memberId).stream()
.map(MemberFCM::getFcmToken)
.toList();
}

private List<Message> createMessages(List<String> tokens, String channelId) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.festago.fcm.application.command;

import com.festago.fcm.domain.MemberFCM;
import com.festago.fcm.domain.MemberFCMExpiredAtPolicy;
import com.festago.fcm.domain.MemberFCMRemoveOldTokensPolicy;
import com.festago.fcm.repository.MemberFCMRepository;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class MemberFCMCommandService {

private final MemberFCMRepository memberFCMRepository;
private final MemberFCMExpiredAtPolicy memberFCMExpiredAtPolicy;
private final MemberFCMRemoveOldTokensPolicy memberFCMDeleteOldTokensPolicy;

// TODO 별도의 Service 클래스 생성해서 비즈니스 로직 이관 생각해 볼 것
public void registerFCM(Long memberId, String fcmToken) {
List<MemberFCM> memberFCMs = memberFCMRepository.findAllByMemberId(memberId);
LocalDateTime expiredAt = memberFCMExpiredAtPolicy.provide();
memberFCMs.stream()
.filter(memberFCM -> memberFCM.isSameToken(fcmToken))
.findAny()
.ifPresentOrElse(memberFCM -> {
memberFCM.changeExpiredAt(expiredAt);
}, () -> {
memberFCMDeleteOldTokensPolicy.delete(memberFCMs);
memberFCMRepository.save(new MemberFCM(memberId, fcmToken, expiredAt));
});
}

public void deleteAllMemberFCM(Long memberId) {
memberFCMRepository.deleteAllByMemberId(memberId);
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

FCM을 등록하는 메서드가 여러 정책에 의해 부피가 조금 큽니다. 😂
따라서 별도의 서비스 클래스로 나눠야 할 것 같기도 하네요.

37 changes: 30 additions & 7 deletions backend/src/main/java/com/festago/fcm/domain/MemberFCM.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
import jakarta.persistence.UniqueConstraint;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@Entity
@Table(
Expand All @@ -23,6 +27,7 @@
}
)
})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberFCM extends BaseTimeEntity {

private static final int MAX_FCM_TOKEN_LENGTH = 255;
Expand All @@ -40,23 +45,24 @@ public class MemberFCM extends BaseTimeEntity {
@Column(name = "fcm_token")
private String fcmToken;

protected MemberFCM() {
}
private LocalDateTime expiredAt;

public MemberFCM(Long memberId, String fcmToken) {
this(null, memberId, fcmToken);
public MemberFCM(Long memberId, String fcmToken, LocalDateTime expiredAt) {
this(null, memberId, fcmToken, expiredAt);
}

public MemberFCM(Long id, Long memberId, String fcmToken) {
validate(memberId, fcmToken);
public MemberFCM(Long id, Long memberId, String fcmToken, LocalDateTime expiredAt) {
validate(memberId, fcmToken, expiredAt);
this.id = id;
this.memberId = memberId;
this.fcmToken = fcmToken;
this.expiredAt = expiredAt;
}

private void validate(Long memberId, String fcmToken) {
private void validate(Long memberId, String fcmToken, LocalDateTime expiredAt) {
validateMemberId(memberId);
validateFcmToken(fcmToken);
validateExpiredAt(expiredAt);
}

private void validateMemberId(Long memberId) {
Expand All @@ -69,6 +75,19 @@ private void validateFcmToken(String fcmToken) {
Validator.maxLength(fcmToken, MAX_FCM_TOKEN_LENGTH, fieldName);
}

private void validateExpiredAt(LocalDateTime expiredAt) {
Validator.notNull(expiredAt, "expiredAt");
}

public void changeExpiredAt(LocalDateTime expiredAt) {
validateExpiredAt(expiredAt);
this.expiredAt = expiredAt;
}

public boolean isSameToken(String fcmToken) {
return Objects.equals(this.fcmToken, fcmToken);
}

public Long getId() {
return id;
}
Expand All @@ -80,4 +99,8 @@ public Long getMemberId() {
public String getFcmToken() {
return fcmToken;
}

public LocalDateTime getExpiredAt() {
return expiredAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.festago.fcm.domain;

import java.time.LocalDateTime;

public interface MemberFCMExpiredAtPolicy {

LocalDateTime provide();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.festago.fcm.domain;

import java.time.Clock;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class MemberFCMExpiredAtPolicyImpl implements MemberFCMExpiredAtPolicy {

private static final int EXPIRED_DAY_OFFSET = 180;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

자문자답인 것 같긴 한데..
보통 2학기에 축제가 9월에 열리니.. 내년 5월에 알림을 받으려면 1년 정도로 하는게 좋을 것 같네요. 😂

private final Clock clock;

@Override
public LocalDateTime provide() {
return LocalDateTime.now(clock).plusDays(EXPIRED_DAY_OFFSET);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.festago.fcm.domain;

import com.festago.fcm.repository.MemberFCMRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class MemberFCMRemoveOldTokensPolicy {

private final MemberFCMRepository memberFCMRepository;

public void delete(List<MemberFCM> memberFCMs) {
memberFCMRepository.deleteByIn(memberFCMs);
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

N개의 FCM 토큰을 허용한다면 아마 다음과 같이 로직을 변경할 수 있을 것 같네요.

var oldTokens = memberFCMs.stream()
    .sorted(comparing(MemberFCM::getExpiredAt).reversed()) // 만료 시간 내림차순
    .skip(n - 1) // 최대 소지 개수 - 1 개를 남기고 모두 삭제
    .toList();
memberFCMRepository.deleteByIn(oldTokens);

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.festago.fcm.dto.v1;

public record MemberFCMTokenRegisterV1Request(
String fcmToken
) {

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.festago.fcm.presentation.v1;

import com.festago.auth.annotation.MemberAuth;
import com.festago.auth.domain.authentication.MemberAuthentication;
import com.festago.fcm.application.command.MemberFCMCommandService;
import com.festago.fcm.dto.v1.MemberFCMTokenRegisterV1Request;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/fcm")
@RequiredArgsConstructor
public class MemberFCMV1Controller {

private final MemberFCMCommandService memberFCMCommandService;

@MemberAuth
@PostMapping
public ResponseEntity<Void> registerFCM(
MemberAuthentication memberAuthentication,
@RequestBody MemberFCMTokenRegisterV1Request request
) {
memberFCMCommandService.registerFCM(memberAuthentication.getId(), request.fcmToken());
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@

import com.festago.fcm.domain.MemberFCM;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.Param;

public interface MemberFCMRepository extends JpaRepository<MemberFCM, Long> {
public interface MemberFCMRepository extends Repository<MemberFCM, Long> {

MemberFCM save(MemberFCM memberFCM);

List<MemberFCM> findAllByMemberId(Long memberId);

boolean existsByMemberIdAndFcmToken(Long memberId, String fcmToken);
@Modifying
@Query("delete from MemberFCM mf where mf.memberId = :memberId")
void deleteAllByMemberId(@Param("memberId") Long memberId);

void deleteAllByMemberId(Long memberId);
@Modifying
@Query("delete from MemberFCM mf where mf in :memberFCMs")
void deleteByIn(List<MemberFCM> memberFCMs);
}
Loading
Loading