Skip to content

Commit f59786c

Browse files
authored
Merge pull request #98 from Central-MakeUs/refactor/#97-improve-info-log
[#97] ♻️ Refactor: 로그 기록 방식 변경, FCM 전송 관련 메트릭 추가
2 parents a673af6 + 72e349c commit f59786c

File tree

5 files changed

+194
-73
lines changed

5 files changed

+194
-73
lines changed

src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java

Lines changed: 85 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import akuma.whiplash.infrastructure.firebase.dto.FcmSendResult;
99
import akuma.whiplash.infrastructure.redis.RedisService;
1010
import akuma.whiplash.global.log.NoMethodLog;
11+
import io.micrometer.core.instrument.Counter;
12+
import io.micrometer.core.instrument.MeterRegistry;
13+
import jakarta.annotation.PostConstruct;
1114
import java.time.LocalDateTime;
1215
import java.time.temporal.ChronoUnit;
1316
import java.util.HashSet;
@@ -28,59 +31,93 @@ public class AlarmReminderScheduler {
2831
private final RedisService redisService;
2932
private final FcmService fcmService;
3033
private final AlarmCommandService alarmCommandService;
34+
private final MeterRegistry meterRegistry;
35+
36+
private Counter preAlarmPushAttemptCounter;
37+
private Counter preAlarmPushSuccessCounter;
38+
private Counter preAlarmPushFailureCounter;
39+
private Counter invalidFcmTokenCounter;
40+
41+
@PostConstruct
42+
void registerMetrics() {
43+
preAlarmPushAttemptCounter = meterRegistry.counter(
44+
"pre_alarm.push_attempt", "scheduler", "pre-alarm");
45+
46+
preAlarmPushSuccessCounter = meterRegistry.counter(
47+
"pre_alarm.push_success", "scheduler", "pre-alarm");
48+
49+
preAlarmPushFailureCounter = meterRegistry.counter(
50+
"pre_alarm.push_failure", "scheduler", "pre-alarm");
51+
52+
invalidFcmTokenCounter = meterRegistry.counter(
53+
"pre_alarm.invalid_token_removed", "scheduler", "pre-alarm");
54+
}
3155

3256
// 매 분 마다 실행
3357
@Scheduled(cron = "0 * * * * *")
3458
@NoMethodLog
3559
public void sendPreAlarmNotifications() {
36-
log.info("알람 울리기 1시간 전 푸시 알림 전송 스케줄러 시작");
37-
try {
38-
// 0. 시간 기준 (분 단위 정렬)
39-
LocalDateTime now = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES);
40-
LocalDateTime windowStart = now.plusMinutes(59);
41-
LocalDateTime windowEnd = now.plusMinutes(61);
42-
43-
// 1. DB에서 알림 대상 필터링 (자정 크로스 안전)
44-
List<OccurrencePushInfo> infos = alarmQueryService.getPreNotificationTargets(windowStart, windowEnd);
45-
46-
if (infos.isEmpty())
47-
return;
48-
49-
// 2. Redis에서 FCM 토큰 조회 → PushTargetDto 변환
50-
List<PushTargetDto> targets = infos.stream()
51-
.flatMap(info ->
52-
redisService.getFcmTokens(info.memberId()).stream()
53-
.map(token -> PushTargetDto.builder()
54-
.token(token)
55-
.address(info.address())
56-
.memberId(info.memberId())
57-
.occurrenceId(info.occurrenceId())
58-
.build()
59-
)
60-
)
61-
.toList(); // <-- 여기서 한 번만 호출
62-
63-
if (targets.isEmpty())
64-
return;
65-
66-
FcmSendResult result = fcmService.sendBulkNotification(targets);
67-
68-
// 3. 성공한 occurrence만 reminderSent=true 벌크 업데이트
69-
if (!result.getSuccessOccurrenceIds().isEmpty()) {
70-
alarmCommandService.markReminderSent(result.getSuccessOccurrenceIds());
71-
}
72-
73-
// 4. 무효 토큰 정리: 각 회원의 토큰 중 무효한 것만 제거
74-
Set<String> invalidTokenSet = new HashSet<>(result.getInvalidTokens());
75-
for (Map.Entry<Long, List<String>> e : result.getMemberToTokens().entrySet()) {
76-
Long memberId = e.getKey();
77-
e.getValue().stream()
78-
.filter(invalidTokenSet::contains)
79-
.forEach(token -> redisService.removeInvalidToken(memberId, token));
80-
}
81-
82-
} finally {
83-
log.info("알람 울리기 1시간 전 푸시 알림 전송 스케줄러 종료");
60+
61+
// 0. 시간 기준 (분 단위 정렬)
62+
LocalDateTime now = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES);
63+
LocalDateTime windowStart = now.plusMinutes(59);
64+
LocalDateTime windowEnd = now.plusMinutes(61);
65+
66+
// 1. DB에서 알림 대상 필터링 (자정 크로스 안전)
67+
List<OccurrencePushInfo> infos = alarmQueryService.getPreNotificationTargets(windowStart, windowEnd);
68+
69+
if (infos.isEmpty())
70+
return;
71+
72+
// 2. Redis에서 FCM 토큰 조회 → push 대상 생성
73+
List<PushTargetDto> targets = infos.stream()
74+
.flatMap(info ->
75+
redisService.getFcmTokens(info.memberId()).stream()
76+
.map(token -> PushTargetDto.builder()
77+
.token(token)
78+
.address(info.address())
79+
.memberId(info.memberId())
80+
.occurrenceId(info.occurrenceId())
81+
.build()
82+
)
83+
)
84+
.toList();
85+
86+
if (targets.isEmpty())
87+
return;
88+
89+
// 실제 전송 시도한 횟수 기록
90+
preAlarmPushAttemptCounter.increment(targets.size());
91+
92+
log.info("알람 울리기 1시간 전 푸시 알림 대상 {}건 전송 시도", targets.size());
93+
94+
// 3. FCM 전송
95+
FcmSendResult result = fcmService.sendBulkNotification(targets);
96+
97+
// 전송 성공/실패 횟수 기록
98+
preAlarmPushSuccessCounter.increment(result.getSuccessCount());
99+
preAlarmPushFailureCounter.increment(result.getFailedCount());
100+
101+
// 4. 성공한 occurrence만 reminderSent=true 벌크 업데이트
102+
if (!result.getSuccessOccurrenceIds().isEmpty()) {
103+
alarmCommandService.markReminderSent(result.getSuccessOccurrenceIds());
104+
}
105+
106+
// 5. 무효 토큰 정리: 각 회원의 토큰 중 무효한 것만 제거
107+
Set<String> invalidTokenSet = new HashSet<>(result.getInvalidTokens());
108+
int invalidCount = 0;
109+
110+
for (Map.Entry<Long, List<String>> e : result.getMemberToTokens().entrySet()) {
111+
Long memberId = e.getKey();
112+
invalidCount += (int) e.getValue().stream()
113+
.filter(invalidTokenSet::contains)
114+
.peek(token -> redisService.removeInvalidToken(memberId, token))
115+
.count();
116+
}
117+
118+
// 제거된 무효 토큰 개수 기록
119+
if (invalidCount > 0) {
120+
invalidFcmTokenCounter.increment(invalidCount);
84121
}
85122
}
86123

src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmRingingNotificationScheduler.java

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
import akuma.whiplash.domains.alarm.application.dto.etc.RingingPushTargetDto;
55
import akuma.whiplash.domains.alarm.domain.service.AlarmQueryService;
66
import akuma.whiplash.infrastructure.firebase.FcmService;
7+
import akuma.whiplash.infrastructure.firebase.dto.FcmMetricResult;
78
import akuma.whiplash.infrastructure.redis.RedisService;
89
import akuma.whiplash.global.log.NoMethodLog;
10+
import io.micrometer.core.instrument.Counter;
11+
import io.micrometer.core.instrument.MeterRegistry;
12+
import jakarta.annotation.PostConstruct;
913
import java.util.List;
1014
import lombok.RequiredArgsConstructor;
1115
import lombok.extern.slf4j.Slf4j;
@@ -20,34 +24,75 @@ public class AlarmRingingNotificationScheduler {
2024
private final AlarmQueryService alarmQueryService;
2125
private final RedisService redisService;
2226
private final FcmService fcmService;
27+
private final MeterRegistry meterRegistry;
28+
29+
private Counter ringingPushAttemptCounter;
30+
private Counter ringingPushSuccessCounter;
31+
private Counter ringingPushFailureCounter;
32+
//private Counter ringingInvalidTokenCounter;
33+
34+
@PostConstruct
35+
void registerMetrics() {
36+
ringingPushAttemptCounter = meterRegistry.counter(
37+
"ringing_alarm.push_attempt", "scheduler", "alarm-ringing");
38+
39+
ringingPushSuccessCounter = meterRegistry.counter(
40+
"ringing_alarm.push_success", "scheduler", "alarm-ringing");
41+
42+
ringingPushFailureCounter = meterRegistry.counter(
43+
"ringing_alarm.push_failure", "scheduler", "alarm-ringing");
44+
45+
/*ringingInvalidTokenCounter = meterRegistry.counter(
46+
"ringing_alarm.invalid_token_removed", "scheduler", "alarm-ringing");*/
47+
}
2348

2449
// 10초 간격으로 실행
2550
@Scheduled(fixedRate = 10000, zone = "Asia/Seoul")
2651
@NoMethodLog
2752
public void sendRingingAlarmNotifications() {
28-
log.info("알람 울림 푸시 알림 전송 스케줄러 시작");
29-
try {
30-
List<RingingPushInfo> infos = alarmQueryService.getRingingNotificationTargets();
31-
if (infos.isEmpty()) {
32-
return;
33-
}
34-
35-
List<RingingPushTargetDto> targets = infos.stream()
36-
.flatMap(info -> redisService.getFcmTokens(info.memberId()).stream()
37-
.map(token -> RingingPushTargetDto.builder()
38-
.token(token)
39-
.alarmId(info.alarmId())
40-
.memberId(info.memberId())
41-
.build()))
42-
.toList();
43-
44-
if (targets.isEmpty()) {
45-
return;
46-
}
47-
48-
fcmService.sendRingingNotifications(targets);
49-
} finally {
50-
log.info("알람 울림 푸시 알림 전송 스케줄러 종료");
53+
54+
List<RingingPushInfo> infos = alarmQueryService.getRingingNotificationTargets();
55+
if (infos.isEmpty()) {
56+
return;
57+
}
58+
59+
List<RingingPushTargetDto> targets = infos.stream()
60+
.flatMap(info -> redisService.getFcmTokens(info.memberId()).stream()
61+
.map(token -> RingingPushTargetDto.builder()
62+
.token(token)
63+
.alarmId(info.alarmId())
64+
.memberId(info.memberId())
65+
.build()))
66+
.toList();
67+
68+
if (targets.isEmpty()) {
69+
return;
70+
}
71+
72+
// 전송 시도 횟수 기록
73+
ringingPushAttemptCounter.increment(targets.size());
74+
75+
log.info("알람 울림 푸시 알림 대상 {}건 전송 시도", targets.size());
76+
77+
FcmMetricResult result = fcmService.sendRingingNotifications(targets);
78+
79+
// 성공/실패 횟수 기록
80+
ringingPushSuccessCounter.increment(result.getSuccessCount());
81+
ringingPushFailureCounter.increment(result.getFailedCount());
82+
83+
/*
84+
// 무효 토큰 제거된 개수 카운트
85+
if (!result.getInvalidTokens().isEmpty()) {
86+
ringingInvalidTokenCounter.increment(result.getInvalidTokens().size());
87+
}
88+
89+
// Redis에서 무효 토큰 제거
90+
for (Map.Entry<Long, List<String>> e : result.getMemberToTokens().entrySet()) {
91+
Long memberId = e.getKey();
92+
e.getValue().stream()
93+
.filter(result.getInvalidTokens()::contains)
94+
.forEach(token -> redisService.removeInvalidToken(memberId, token));
5195
}
96+
*/
5297
}
5398
}

src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import akuma.whiplash.domains.alarm.application.dto.etc.PushTargetDto;
44
import akuma.whiplash.domains.alarm.application.dto.etc.RingingPushTargetDto;
5+
import akuma.whiplash.infrastructure.firebase.dto.FcmMetricResult;
56
import akuma.whiplash.infrastructure.firebase.dto.FcmSendResult;
67
import akuma.whiplash.infrastructure.redis.RedisService;
78
import com.google.firebase.messaging.AndroidConfig;
@@ -60,6 +61,8 @@ public FcmSendResult sendBulkNotification(List<PushTargetDto> targets) {
6061
.successOccurrenceIds(Set.of())
6162
.invalidTokens(List.of())
6263
.memberToTokens(Map.of())
64+
.successCount(0)
65+
.failedCount(0)
6366
.build();
6467
}
6568

@@ -73,6 +76,9 @@ public FcmSendResult sendBulkNotification(List<PushTargetDto> targets) {
7376
List<String> invalidTokens = new ArrayList<>();
7477
Map<Long, List<String>> memberToTokens = new HashMap<>();
7578

79+
int totalSuccessCount = 0
80+
, totalFailureCount = 0;
81+
7682
for (Map.Entry<String, List<PushTargetDto>> entry : groupedByBody.entrySet()) {
7783
String body = entry.getKey();
7884
List<PushTargetDto> group = dedupByToken(entry.getValue(), PushTargetDto::token);
@@ -105,6 +111,9 @@ public FcmSendResult sendBulkNotification(List<PushTargetDto> targets) {
105111
BatchResponse response = FirebaseMessaging.getInstance().sendEachForMulticast(message);
106112
log.info("사전 알림 FCM 멀티캐스트 결과: success={}, failure={}", response.getSuccessCount(), response.getFailureCount());
107113

114+
totalSuccessCount += response.getSuccessCount();
115+
totalFailureCount += response.getFailureCount();
116+
108117
handleSendResult(response.getResponses(), batch, successOccurrenceIds, invalidTokens, memberToTokens);
109118
} catch (FirebaseMessagingException e) {
110119
log.error("FCM 전송 실패(멀티캐스트 전체). body={}", body, e);
@@ -116,21 +125,29 @@ public FcmSendResult sendBulkNotification(List<PushTargetDto> targets) {
116125
.successOccurrenceIds(successOccurrenceIds)
117126
.invalidTokens(invalidTokens)
118127
.memberToTokens(memberToTokens)
128+
.successCount(totalSuccessCount)
129+
.failedCount(totalFailureCount)
119130
.build();
120131
}
121132

122133
/**
123134
* 알람 울릴 때 FCM 푸시 알림 전송
124135
* @param targets
125136
*/
126-
public void sendRingingNotifications(List<RingingPushTargetDto> targets) {
137+
public FcmMetricResult sendRingingNotifications(List<RingingPushTargetDto> targets) {
127138
if (targets == null || targets.isEmpty()) {
128-
return;
139+
return FcmMetricResult.builder()
140+
.successCount(0)
141+
.failedCount(0)
142+
.build();
129143
}
130144

131145
Map<Long, List<RingingPushTargetDto>> groupedByAlarm = targets.stream()
132146
.collect(Collectors.groupingBy(RingingPushTargetDto::alarmId));
133147

148+
int totalSuccessCount = 0
149+
, totalFailureCount = 0;
150+
134151
for (Map.Entry<Long, List<RingingPushTargetDto>> entry : groupedByAlarm.entrySet()) {
135152
Long alarmId = entry.getKey();
136153
List<RingingPushTargetDto> group = dedupByToken(entry.getValue(), RingingPushTargetDto::token);
@@ -160,12 +177,21 @@ public void sendRingingNotifications(List<RingingPushTargetDto> targets) {
160177
try {
161178
BatchResponse response = FirebaseMessaging.getInstance().sendEachForMulticast(message);
162179
log.info("알람 울림 FCM 멀티캐스트 결과: success={}, failure={}", response.getSuccessCount(), response.getFailureCount());
180+
181+
totalSuccessCount += response.getSuccessCount();
182+
totalFailureCount += response.getFailureCount();
183+
163184
handleRingingSendResult(response.getResponses(), batch);
164185
} catch (FirebaseMessagingException e) {
165186
log.error("FCM 전송 실패(알람 울림)", e);
166187
}
167188
}
168189
}
190+
191+
return FcmMetricResult.builder()
192+
.successCount(totalSuccessCount)
193+
.failedCount(totalFailureCount)
194+
.build();
169195
}
170196

171197
private void handleRingingSendResult(
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package akuma.whiplash.infrastructure.firebase.dto;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@Builder
8+
public class FcmMetricResult {
9+
private final int successCount;
10+
private final int failedCount;
11+
}

src/main/java/akuma/whiplash/infrastructure/firebase/dto/FcmSendResult.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ public class FcmSendResult {
1212
private final Set<Long> successOccurrenceIds; // 적어도 1개 토큰 전송 성공한 occurrence
1313
private final List<String> invalidTokens; // 등록 말소 대상 토큰
1414
private final Map<Long, List<String>> memberToTokens; // (선택) 멤버별 성공 토큰 집계
15+
private final int successCount;
16+
private final int failedCount;
1517
}

0 commit comments

Comments
 (0)