-
Notifications
You must be signed in to change notification settings - Fork 0
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
refactor: fcm 푸시 알림 리팩토링 #110
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
1fdc650
feat: 디바이스 관련 엔티티 정의
kimyu0218 7ad3718
chore: DB 마이그레이션 파일 작성
kimyu0218 80b4347
rename: PushNotificationEventHandler -> PushMessageEventHandler
kimyu0218 b3742a1
feat: deviceIdentifier 추가
kimyu0218 b678fc4
refactor: DTO 및 디바이스 엔티티 추가에 따른 수정
kimyu0218 59ed736
feat: Device 엔티티 추가 및 Member 엔티티 수정
kimyu0218 a75585f
refactor: 함수 분리 및 로직 변경에 따른 수정
kimyu0218 32f503f
test: DeviceSubscription 관련 테스트 작성
kimyu0218 f8201bd
remove: 불필요한 코드 삭제
kimyu0218 f656040
refactor: MissionRetryPushMessageService 분리
kimyu0218 7c73af2
feat: 멀티 디바이스 및 다중 로그인 지원을 위한 redis 저장소 수정
kimyu0218 bc7cdee
refactor: deprecatedDeviceToken -> deviceIdentifier
kimyu0218 66abd52
feat: 디바이스 일급 컬렉션 작성
kimyu0218 a787baf
chore: 디바이스 관련 에러 코드 추가
kimyu0218 c153574
move: firebase -> device
kimyu0218 11e2ae4
refactor: MissionMemberService 로직의 구독 부분 DeviceSubscriptionService로 분리
kimyu0218 baf811a
refactor: 구독 의존성 개선
kimyu0218 003f532
refactor: data 메시지 -> notification+data 혼합 메시지
kimyu0218 66eff4e
test: 메시지 타입 변경에 따른 테스트 수정
kimyu0218 6a9a84a
refactor: deviceIdentifier & osType 추가
kimyu0218 4c947ae
refactor: member, device 도메인 분리
kimyu0218 b941ce6
chore: 유효성 검사 추가
kimyu0218 bee5757
fix: 유효성 검사 수정
kimyu0218 44d1a00
test: MissionRetryMessageRepository 테스트
kimyu0218 2c0eb79
refactor: 이전 버전 지원, 함수 중복 제거
kimyu0218 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 2 additions & 1 deletion
3
src/main/java/com/nexters/goalpanzi/application/auth/dto/request/AppleLoginCommand.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package com.nexters.goalpanzi.application.auth.dto.request; | ||
|
||
public record AppleLoginCommand( | ||
String identityToken | ||
String identityToken, | ||
String deviceIdentifier | ||
) { | ||
} |
3 changes: 2 additions & 1 deletion
3
src/main/java/com/nexters/goalpanzi/application/auth/dto/request/GoogleLoginCommand.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package com.nexters.goalpanzi.application.auth.dto.request; | ||
|
||
public record GoogleLoginCommand( | ||
String email | ||
String email, | ||
String deviceIdentifier | ||
) { | ||
} |
7 changes: 7 additions & 0 deletions
7
src/main/java/com/nexters/goalpanzi/application/auth/event/LoginEvent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.nexters.goalpanzi.application.auth.event; | ||
|
||
public record LoginEvent( | ||
Long memberId, | ||
String deviceIdentifier | ||
) { | ||
} |
68 changes: 68 additions & 0 deletions
68
src/main/java/com/nexters/goalpanzi/application/device/DeviceService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package com.nexters.goalpanzi.application.device; | ||
|
||
import com.nexters.goalpanzi.application.device.dto.request.UpdateDeviceTokenCommand; | ||
import com.nexters.goalpanzi.application.device.dto.request.UpdatePushActivationStatusCommand; | ||
import com.nexters.goalpanzi.application.device.event.UpdateDeviceTokenEvent; | ||
import com.nexters.goalpanzi.application.device.event.UpdatePushActivationStatusEvent; | ||
import com.nexters.goalpanzi.domain.device.Device; | ||
import com.nexters.goalpanzi.domain.device.OsType; | ||
import com.nexters.goalpanzi.domain.device.repository.DeviceRepository; | ||
import com.nexters.goalpanzi.domain.member.Member; | ||
import com.nexters.goalpanzi.domain.member.repository.MemberRepository; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.context.ApplicationEventPublisher; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Transactional(readOnly = true) | ||
@RequiredArgsConstructor | ||
@Service | ||
public class DeviceService { | ||
|
||
private final MemberRepository memberRepository; | ||
private final DeviceRepository deviceRepository; | ||
|
||
private final ApplicationEventPublisher eventPublisher; | ||
|
||
@Transactional | ||
public void updateDeviceToken(final UpdateDeviceTokenCommand command) { | ||
if (command.deviceIdentifier().isBlank() // TODO: 추후 앞의 조건 삭제 | ||
|| !deviceRepository.existsByDeviceIdentifier(command.deviceIdentifier())) { | ||
createDevice(command.memberId(), command.deviceIdentifier(), command.deviceToken(), command.osType()); | ||
} else { | ||
updateDevice(command.memberId(), command.deviceIdentifier(), command.deviceToken()); | ||
} | ||
} | ||
|
||
private void createDevice(final Long memberId, final String deviceIdentifier, final String deviceToken, final OsType osType) { | ||
Member member = memberRepository.getMember(memberId); | ||
Device device = deviceRepository.save(new Device(member, deviceIdentifier, deviceToken, osType)); | ||
|
||
eventPublisher.publishEvent( | ||
new UpdateDeviceTokenEvent(memberId, device.getId(), null) | ||
); | ||
} | ||
|
||
private void updateDevice(final Long memberId, final String deviceIdentifier, final String deviceToken) { | ||
Device device = deviceRepository.getDevice(memberId, deviceIdentifier); | ||
|
||
String deprecatedToken = device.getDeviceToken(); | ||
device.updateDeviceToken(deviceToken); | ||
device.updatePushActivationStatus(true); | ||
|
||
eventPublisher.publishEvent( | ||
new UpdateDeviceTokenEvent(memberId, device.getId(), deprecatedToken) | ||
); | ||
} | ||
|
||
@Transactional | ||
public void updatePushActivationStatus(final UpdatePushActivationStatusCommand command) { | ||
Device device = deviceRepository.getDevice(command.memberId(), command.deviceIdentifier()); | ||
|
||
device.updatePushActivationStatus(command.pushActivationStatus()); | ||
|
||
eventPublisher.publishEvent( | ||
new UpdatePushActivationStatusEvent(command.memberId(), device.getId(), command.pushActivationStatus(), device.getDeviceToken()) | ||
); | ||
} | ||
} |
238 changes: 238 additions & 0 deletions
238
src/main/java/com/nexters/goalpanzi/application/device/DeviceSubscriptionService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,238 @@ | ||
package com.nexters.goalpanzi.application.device; | ||
|
||
import com.nexters.goalpanzi.application.firebase.TopicGenerator; | ||
import com.nexters.goalpanzi.application.mission.event.CancelMissionRetryPushMessageEvent; | ||
import com.nexters.goalpanzi.application.mission.event.ReserveMissionRetryPushMessageEvent; | ||
import com.nexters.goalpanzi.application.mission.event.UnsubscribeFromMissionEvent; | ||
import com.nexters.goalpanzi.application.mission.event.UpdateMissionRetryPushMessageEvent; | ||
import com.nexters.goalpanzi.domain.device.Device; | ||
import com.nexters.goalpanzi.domain.device.DeviceSubscription; | ||
import com.nexters.goalpanzi.domain.device.Devices; | ||
import com.nexters.goalpanzi.domain.device.repository.DeviceRepository; | ||
import com.nexters.goalpanzi.domain.device.repository.DeviceSubscriptionRepository; | ||
import com.nexters.goalpanzi.domain.member.Member; | ||
import com.nexters.goalpanzi.domain.mission.Mission; | ||
import com.nexters.goalpanzi.domain.mission.MissionMember; | ||
import com.nexters.goalpanzi.domain.mission.MissionStatus; | ||
import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository; | ||
import com.nexters.goalpanzi.domain.mission.repository.MissionRepository; | ||
import com.nexters.goalpanzi.infrastructure.firebase.TopicSubscriber; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.context.ApplicationEventPublisher; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
import java.util.List; | ||
import java.util.stream.Collectors; | ||
|
||
import static com.nexters.goalpanzi.domain.mission.MissionStatus.*; | ||
|
||
@Transactional(readOnly = true) | ||
@RequiredArgsConstructor | ||
@Service | ||
public class DeviceSubscriptionService { | ||
|
||
private final DeviceRepository deviceRepository; | ||
private final DeviceSubscriptionRepository deviceSubscriptionRepository; | ||
private final MissionRepository missionRepository; | ||
private final MissionMemberRepository missionMemberRepository; | ||
|
||
private final ApplicationEventPublisher eventPublisher; | ||
private final TopicSubscriber topicSubscriber; | ||
|
||
private static final List<MissionStatus> SUBSCRIBABLE_MISSION_STATUS | ||
= List.of(CREATED, IN_PROGRESS, PENDING_COMPLETION); | ||
|
||
/** | ||
* <b>새로운 미션 참여 시 미션 구독 시작</b><br> | ||
* 멤버의 디바이스 중 푸시가 활성화된 디바이스를 대상으로 미션 구독 | ||
* | ||
* @param memberId 멤버 아이디 | ||
* @param mission 새롭게 참여한 미션 | ||
*/ | ||
@Transactional | ||
public void subscribeToMission(final Long memberId, final Mission mission) { | ||
Devices devices = new Devices( | ||
deviceRepository.findAllByMemberId(memberId) | ||
); | ||
|
||
devices.getActivatedDevices() | ||
.forEach(device -> | ||
deviceSubscriptionRepository.save(new DeviceSubscription(device, mission)) | ||
); | ||
topicSubscriber.subscribeToTopic( | ||
devices.getActivatedDeviceTokens(), TopicGenerator.getTopic(mission.getId()) | ||
); | ||
} | ||
|
||
/** | ||
* <b>미션 취소/종료 시 미션 구독 취소</b><br> | ||
* 해당 미션을 구독한 디바이스를 대상으로 구독 취소 | ||
* | ||
* @param missionId 취소/종료된 미션 | ||
*/ | ||
@Transactional | ||
public void unsubscribeFromMission(final Long missionId) { | ||
List<String> deviceTokens = findTopicSubscribers(missionId); | ||
|
||
deviceSubscriptionRepository.deleteAllByMissionId(missionId); | ||
topicSubscriber.unsubscribeFromTopic(deviceTokens, TopicGenerator.getTopic(missionId)); | ||
} | ||
|
||
private List<String> findTopicSubscribers(final Long missionId) { | ||
List<DeviceSubscription> subscriptions = deviceSubscriptionRepository.findAllWithDeviceAndMissionByMissionId(missionId); | ||
|
||
return subscriptions.stream() | ||
.map(it -> it.getDevice().getDeviceToken()) | ||
.toList(); | ||
} | ||
|
||
/** | ||
* <b>디바이스 토큰을 갱신하거나 푸시 알림 활성화 시 새로운 디바이스 토큰으로 내 미션 구독 시작</b><br> | ||
* + UpdateMissionRetryPushMessageEvent를 통해 예약된 메시지의 디바이스 토큰 갱신 | ||
* | ||
* @param memberId 멤버 아이디 | ||
* @param deviceId 디바이스 아이디 | ||
*/ | ||
@Transactional | ||
public void subscribeToMyMissions(final Long memberId, final Long deviceId) { | ||
Device device = deviceRepository.getDevice(deviceId); | ||
List<String> topics = findMySubscribedTopics(deviceId); | ||
List<Mission> missions = missionRepository.findAllById( | ||
findMySubscribableMission(memberId, topics) | ||
); | ||
|
||
topics.forEach(topic -> | ||
topicSubscriber.subscribeToTopic(List.of(device.getDeviceToken()), topic) | ||
); | ||
missions.forEach(mission -> { | ||
deviceSubscriptionRepository.save(new DeviceSubscription(device, mission)); | ||
|
||
String topic = TopicGenerator.getTopic(mission.getId()); | ||
topicSubscriber.subscribeToTopic(List.of(device.getDeviceToken()), topic); | ||
}); | ||
eventPublisher.publishEvent( | ||
new UpdateMissionRetryPushMessageEvent(memberId, device.getDeviceToken()) | ||
); | ||
} | ||
|
||
/** | ||
* <b>로그인 시 기존 디바이스가 구독한 미션 구독 취소</b> | ||
* + CancelMissionRetryPushMessageEvent를 통해 예약된 메시지 취소 | ||
* | ||
* @param memberId 멤버 아이디 | ||
* @param deviceIdentifier 디바이스 식별자 | ||
*/ | ||
@Transactional | ||
public void unsubscribeFromMyMissions(final Long memberId, final String deviceIdentifier) { | ||
Devices devices = new Devices( | ||
deviceRepository.findAllByDeviceIdentifier(deviceIdentifier) | ||
); | ||
List<String> topics = devices.getActivatedDevices().stream() | ||
.flatMap(device -> findMySubscribedTopics(device.getId()).stream()) | ||
.toList(); | ||
|
||
topics.forEach(topic -> | ||
topicSubscriber.unsubscribeFromTopic(devices.getActivatedDeviceTokens(), topic) | ||
); | ||
eventPublisher.publishEvent( | ||
new CancelMissionRetryPushMessageEvent(memberId) | ||
); | ||
} | ||
|
||
/** | ||
* <b>디바이스 토큰을 갱신하거나 푸시 비활성화 시 기존 디바이스 토큰으로 구독한 미션 구독 취소</b><br> | ||
* + CancelMissionRetryPushMessageEvent를 통해 예약된 메시지 취소 | ||
* | ||
* @param memberId 멤버 아이디 | ||
* @param deviceId 디바이스 아이디 | ||
* @param deviceToken 기존 디바이스 토큰 | ||
*/ | ||
@Transactional | ||
public void unsubscribeFromMyMissions(final Long memberId, final Long deviceId, final String deviceToken) { | ||
List<String> topics = findMySubscribedTopics(deviceId); | ||
|
||
topics.forEach(topic -> | ||
topicSubscriber.unsubscribeFromTopic(List.of(deviceToken), topic) | ||
); | ||
eventPublisher.publishEvent( | ||
new CancelMissionRetryPushMessageEvent(memberId) | ||
); | ||
} | ||
|
||
private List<String> findMySubscribedTopics(final Long deviceId) { | ||
List<DeviceSubscription> subscriptions = deviceSubscriptionRepository.findAllWithMissionAndDeviceByDeviceId(deviceId); | ||
|
||
return subscriptions.stream() | ||
.map(it -> TopicGenerator.getTopic(it.getMission().getId())) | ||
.toList(); | ||
} | ||
|
||
private List<Long> findMySubscribableMission(final Long memberId, List<String> topicFilter) { | ||
List<MissionMember> missionMembers = missionMemberRepository.findAllWithMissionByMemberId(memberId); | ||
List<MissionMember> filteredMissionMembers = missionMembers.stream() | ||
.filter(this::isSubscribableMission) | ||
.filter(it -> isAlreadySubscribedMission(topicFilter, it.getMission().getId())) | ||
.toList(); | ||
|
||
return filteredMissionMembers.stream() | ||
.map(it -> it.getMission().getId()) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
private boolean isSubscribableMission(final MissionMember missionMember) { | ||
return SUBSCRIBABLE_MISSION_STATUS.contains(missionMember.getMissionStatus()); | ||
} | ||
|
||
private boolean isAlreadySubscribedMission(List<String> filter, final Long missionId) { | ||
return filter.contains(TopicGenerator.getTopic(missionId)); | ||
} | ||
|
||
/** | ||
* <b>취소/완료 상태의 미션을 찾아 이벤트 게시</b><br> | ||
* - 취소/완료 상태 : UnsubscribeFromMissionEvent를 게시하여 미션 구독 취소<br> | ||
* - 완료 상태 : ReserveMissionRetryPushMessageEvent를 게시하여 메시지 예약 | ||
*/ | ||
@Transactional | ||
public void unsubscribeFromUselessMissions() { | ||
List<Mission> missions = missionRepository.getInProgressMissions(); | ||
|
||
missions.forEach(mission -> { | ||
List<MissionMember> missionMembers = missionMemberRepository.findAllWithMemberByMissionId(mission.getId()); | ||
|
||
if (isCancelledMission(missionMembers) || isCompletedMission(missionMembers)) { | ||
eventPublisher.publishEvent( | ||
new UnsubscribeFromMissionEvent(mission.getId()) | ||
); | ||
if (isCompletedMission(missionMembers)) { | ||
missionMembers.forEach(missionMember -> | ||
reserveRetryPushMessageForMember(missionMember.getMember()) | ||
); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
private boolean isCancelledMission(final List<MissionMember> missionMembers) { | ||
return missionMembers.stream() | ||
.anyMatch(it -> it.getMissionStatus().equals(CANCELED)); | ||
} | ||
|
||
private boolean isCompletedMission(final List<MissionMember> missionMembers) { | ||
return missionMembers.stream() | ||
.anyMatch(it -> it.getMissionStatus().equals(COMPLETED)); | ||
} | ||
|
||
private void reserveRetryPushMessageForMember(final Member member) { | ||
Devices devices = new Devices( | ||
deviceRepository.findAllByMemberId(member.getId()) | ||
); | ||
|
||
devices.getActivatedDeviceTokens() | ||
.forEach(deviceToken -> | ||
eventPublisher.publishEvent( | ||
new ReserveMissionRetryPushMessageEvent(member.getId(), deviceToken) | ||
) | ||
); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Warning
PATCH 시 insert가 발생합니다
updateDeviceToken
이 실행되었을 때device
테이블을 확인하여 insert나 update를 수행하고 있습니다.createDevice
를 실행하도록 하는 것이 좋을까요?