Skip to content

Commit dfa063f

Browse files
committed
[BOOK-433] refactor: 멀티디바이스에서 유효하지 않은 FID/FCM 토큰 정리 로직 추가 (#129)
1 parent b55b7fc commit dfa063f

File tree

7 files changed

+83
-31
lines changed

7 files changed

+83
-31
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.yapp.batch.service
2+
3+
data class FcmSendResult(
4+
val successCount: Int,
5+
val failureCount: Int,
6+
val invalidTokens: List<String>
7+
)
Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,72 @@
11
package org.yapp.batch.service
22

3-
import com.google.firebase.messaging.FirebaseMessaging
4-
import com.google.firebase.messaging.Message
5-
import com.google.firebase.messaging.Notification
3+
import com.google.firebase.messaging.*
64
import org.slf4j.LoggerFactory
75
import org.springframework.stereotype.Service
86

97
@Service
108
class FcmService {
119
private val logger = LoggerFactory.getLogger(FcmService::class.java)
1210

13-
fun sendNotification(token: String, title: String, body: String): String? {
14-
try {
15-
val notification = Notification.builder()
16-
.setTitle(title)
17-
.setBody(body)
18-
.build()
11+
fun sendMulticastNotification(tokens: List<String>, title: String, body: String): FcmSendResult {
12+
if (tokens.isEmpty()) {
13+
logger.warn("FCM token list is empty. Skipping notification.")
14+
return FcmSendResult(0, 0, emptyList())
15+
}
1916

20-
val message = Message.builder()
21-
.setToken(token)
17+
val notification = buildNotification(title, body)
18+
val messages = tokens.map { token ->
19+
Message.builder()
2220
.setNotification(notification)
21+
.setToken(token)
2322
.build()
23+
}
2424

25-
val response = FirebaseMessaging.getInstance().send(message)
26-
logger.info("Successfully sent message: {}", response)
27-
return response
28-
} catch (e: Exception) {
29-
logger.error("Failed to send FCM notification", e)
30-
return null
25+
try {
26+
val response = FirebaseMessaging.getInstance().sendEach(messages)
27+
return processFcmResponse(response, tokens)
28+
} catch (e: FirebaseMessagingException) {
29+
logger.error("Failed to send FCM notification to ${tokens.size} tokens", e)
30+
return FcmSendResult(0, tokens.size, emptyList())
3131
}
3232
}
3333

34-
fun sendMulticastNotification(tokens: List<String>, title: String, body: String): List<String> {
35-
val successfulSends = mutableListOf<String>()
34+
private fun buildNotification(title: String, body: String): Notification {
35+
return Notification.builder()
36+
.setTitle(title)
37+
.setBody(body)
38+
.build()
39+
}
3640

37-
tokens.forEach { token ->
38-
val messageId = sendNotification(token, title, body)
39-
if (messageId != null) {
40-
successfulSends.add(messageId)
41+
private fun processFcmResponse(response: BatchResponse, tokens: List<String>): FcmSendResult {
42+
val invalidTokens = mutableListOf<String>()
43+
44+
if (response.failureCount > 0) {
45+
response.responses.forEachIndexed { index, sendResponse ->
46+
if (!sendResponse.isSuccessful) {
47+
val failedToken = tokens[index]
48+
val errorCode = sendResponse.exception?.messagingErrorCode
49+
if (errorCode == MessagingErrorCode.UNREGISTERED || errorCode == MessagingErrorCode.INVALID_ARGUMENT) {
50+
invalidTokens.add(failedToken)
51+
logger.warn("Invalid FCM token found: {}. Error: {}", failedToken, errorCode)
52+
} else {
53+
logger.error("Failed to send to token: {}. Error: {}", failedToken, errorCode, sendResponse.exception)
54+
}
55+
}
4156
}
4257
}
4358

44-
return successfulSends
59+
logger.info(
60+
"FCM multicast message sent. Success: {}, Failure: {}, Invalid Tokens: {}",
61+
response.successCount,
62+
response.failureCount,
63+
invalidTokens.size
64+
)
65+
66+
return FcmSendResult(
67+
successCount = response.successCount,
68+
failureCount = response.failureCount,
69+
invalidTokens = invalidTokens
70+
)
4571
}
4672
}

batch/src/main/kotlin/org/yapp/batch/service/NotificationService.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,20 +121,22 @@ class NotificationService(
121121
message: String
122122
): Int {
123123
val validTokens = devices
124-
.filter { it.fcmToken.isNotBlank() }
125124
.map { it.fcmToken }
125+
.filter { it.isNotBlank() }
126126

127127
if (validTokens.isEmpty()) {
128+
logger.warn("No valid FCM tokens found for devices: {}", devices.map { it.id })
128129
return 0
129130
}
130131

131-
return try {
132-
val results = fcmService.sendMulticastNotification(validTokens, title, message)
133-
results.size
134-
} catch (e: Exception) {
135-
logger.error("Error sending notifications to devices", e)
136-
0
132+
val result = fcmService.sendMulticastNotification(validTokens, title, message)
133+
134+
if (result.invalidTokens.isNotEmpty()) {
135+
logger.info("Found ${result.invalidTokens.size} invalid tokens to remove.")
136+
deviceDomainService.removeDevicesByTokens(result.invalidTokens)
137137
}
138+
139+
return result.successCount
138140
}
139141

140142
@Transactional

domain/src/main/kotlin/org/yapp/domain/device/DeviceDomainService.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,10 @@ class DeviceDomainService(
2020
return deviceRepository.findByUserId(userId)
2121
.map { DeviceVO.from(it) }
2222
}
23+
24+
fun removeDevicesByTokens(tokens: List<String>) {
25+
if (tokens.isNotEmpty()) {
26+
deviceRepository.deleteByTokens(tokens)
27+
}
28+
}
2329
}

domain/src/main/kotlin/org/yapp/domain/device/DeviceRepository.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ interface DeviceRepository {
66
fun findByDeviceId(deviceId: String): Device?
77
fun save(device: Device): Device
88
fun findByUserId(userId: UUID): List<Device>
9+
fun deleteByTokens(tokens: List<String>)
910
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
package org.yapp.infra.device
22

33
import org.springframework.data.jpa.repository.JpaRepository
4+
import org.springframework.data.jpa.repository.Modifying
5+
import org.springframework.data.jpa.repository.Query
46
import org.yapp.infra.device.entity.DeviceEntity
57
import java.util.UUID
68

79
interface DeviceJpaRepository : JpaRepository<DeviceEntity, UUID> {
810
fun findByDeviceId(deviceId: String): DeviceEntity?
911
fun findByUserId(userId: UUID): List<DeviceEntity>
12+
13+
@Modifying
14+
@Query("DELETE FROM DeviceEntity d WHERE d.fcmToken IN :tokens")
15+
fun deleteByFcmTokenIn(tokens: List<String>)
1016
}

infra/src/main/kotlin/org/yapp/infra/device/DeviceRepositoryImpl.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ class DeviceRepositoryImpl(
2121
override fun findByUserId(userId: UUID): List<Device> {
2222
return deviceJpaRepository.findByUserId(userId).map { it.toDomain() }
2323
}
24+
25+
override fun deleteByTokens(tokens: List<String>) {
26+
deviceJpaRepository.deleteByFcmTokenIn(tokens)
27+
}
2428
}

0 commit comments

Comments
 (0)