-
Notifications
You must be signed in to change notification settings - Fork 3
FCM 메시지를 받을 유저 필터링 로직 구현 #220
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
Changes from all commits
4da5a74
8a873bd
b24c8b4
da871aa
0905d46
6365616
cfb41aa
c41acfb
4682360
d59a4aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.jootalkpia.state_server.config; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.data.redis.connection.RedisConnectionFactory; | ||
| import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; | ||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; | ||
| import org.springframework.data.redis.serializer.StringRedisSerializer; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Configuration | ||
| public class RedisConfig { | ||
| private final RedisProperties redisProperties; | ||
|
|
||
| @Bean | ||
| public RedisConnectionFactory redisConnectionFactory() { | ||
| return new LettuceConnectionFactory(redisProperties.host(), redisProperties.port()); | ||
| } | ||
| @Bean | ||
| public RedisTemplate<String, String> stringOperRedisTemplate(RedisConnectionFactory connectionFactory) { | ||
| RedisTemplate<String, String> template = new RedisTemplate<>(); | ||
| template.setConnectionFactory(connectionFactory); | ||
|
|
||
| template.setKeySerializer(new StringRedisSerializer()); | ||
| template.setValueSerializer(new StringRedisSerializer()); | ||
|
|
||
| return template; | ||
| } | ||
|
|
||
| @Bean | ||
| public RedisTemplate<String, Object> objectOperRedisTemplate(RedisConnectionFactory connectionFactory) { | ||
| RedisTemplate<String, Object> template = new RedisTemplate<>(); | ||
| template.setConnectionFactory(connectionFactory); | ||
|
|
||
| template.setKeySerializer(new StringRedisSerializer()); | ||
| template.setHashKeySerializer(new StringRedisSerializer()); | ||
|
|
||
| Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(Object.class); | ||
| template.setValueSerializer(jsonSerializer); | ||
| template.setHashValueSerializer(jsonSerializer); | ||
|
|
||
| return template; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.jootalkpia.state_server.config; | ||
|
|
||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
|
|
||
| @ConfigurationProperties(prefix = "spring.data.redis") | ||
| public record RedisProperties( | ||
| String host, | ||
| int port | ||
| ) { | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.jootalkpia.state_server.entity; | ||
|
|
||
| public class RedisKeys { | ||
| private static final String SESSION_PREFIX = "sessions"; | ||
| private static final String USER_PREFIX = "user"; | ||
| private static final String CHANNEL_PREFIX = "channel"; | ||
| private static final String SUBSCRIBERS_PREFIX = "subscribers"; | ||
| private static final String ACTIVE_PREFIX = "active"; | ||
|
|
||
| public static String userSessions(String userId) { | ||
| return USER_PREFIX + ":" + userId + ":" + SESSION_PREFIX; | ||
| } | ||
|
|
||
| public static String channelActive(String channelId) { | ||
| return CHANNEL_PREFIX + ":" + channelId + ":" + ACTIVE_PREFIX; | ||
| } | ||
|
|
||
| public static String channelSubscribers(String channelId) { | ||
| return CHANNEL_PREFIX + ":" + channelId + ":" + SUBSCRIBERS_PREFIX; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,17 @@ | ||
| package com.jootalkpia.state_server.service; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.jootalkpia.state_server.ChatMessageToKafka; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.kafka.annotation.KafkaListener; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| @Slf4j | ||
| @RequiredArgsConstructor | ||
| public class KafkaConsumer { | ||
| private final StateService stateService; | ||
|
|
||
| @KafkaListener( | ||
| topics = "${topic.chat}", | ||
|
|
@@ -20,11 +23,12 @@ public void processState(String kafkaMessage) { | |
| ObjectMapper mapper = new ObjectMapper(); | ||
|
|
||
| try { | ||
| ChatMessageToKafka chatMessageToKafka = mapper.readValue(kafkaMessage, ChatMessageToKafka.class); | ||
| JsonNode rootNode = mapper.readTree(kafkaMessage); | ||
| JsonNode commonNode = rootNode.get("common"); | ||
| JsonNode messagesNode = rootNode.get("message"); | ||
|
Comment on lines
+26
to
+28
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로는 안될까요?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분에 대해선 FCM 발송 로직 구현 때 테스트 해보겠습니다! |
||
|
|
||
| // 유저 상태 검증 로직 | ||
| stateService.findNotificationTargets(commonNode.get("channelId").asText()); | ||
|
|
||
| log.info("dto ===> " + chatMessageToKafka.toString()); | ||
| } catch (Exception ex) { | ||
| log.error(ex.getMessage(), ex); // 추후에 GlobalException 처리 | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| package com.jootalkpia.state_server.service; | ||
|
|
||
| import com.jootalkpia.state_server.entity.RedisKeys; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
|
|
||
| @Service | ||
| @Slf4j | ||
| @RequiredArgsConstructor | ||
| public class StateService { | ||
| private final RedisTemplate<String, String> stringOperRedisTemplate; | ||
| private final RedisTemplate<String, Object> objectOperRedisTemplate; | ||
|
|
||
| public Set<String> findNotificationTargets(String channelId) { | ||
| Set<String> subscriber = findSubscribers(channelId); | ||
| Set<String> onlineSessions = findOnlineSessions(subscriber); | ||
| Set<String> activeSessions = findActiveSessions(channelId, subscriber); | ||
|
|
||
| log.info("[Notification Targets] Channel: {}", channelId); | ||
| log.info("├── Online Sessions: {}", onlineSessions); | ||
| log.info("├── Active Sessions: {}", activeSessions); | ||
|
|
||
| onlineSessions.removeAll(activeSessions); | ||
| log.info("└── Target Sessions: {}", onlineSessions); | ||
|
|
||
| return onlineSessions; | ||
| } | ||
|
|
||
| private Set<String> findSubscribers(String channelId) { | ||
| return stringOperRedisTemplate.opsForSet().members(RedisKeys.channelSubscribers(channelId)); | ||
| } | ||
|
|
||
| private Set<String> findOnlineSessions(Set<String> subscriber) { | ||
| Set<String> onlineSessions = new HashSet<>(); | ||
|
|
||
| for (String userId : subscriber) { | ||
| Set<String> userSessions = stringOperRedisTemplate.opsForSet() | ||
| .members(RedisKeys.userSessions(userId)); | ||
| if (userSessions != null) { | ||
| onlineSessions.addAll(userSessions); | ||
| } | ||
| } | ||
|
|
||
| return onlineSessions; | ||
| } | ||
|
|
||
| private Set<String> findActiveSessions(String channelId, Set<String> subscriber) { | ||
| Set<String> activeSessions = new HashSet<>(); | ||
|
|
||
| for (String userId : subscriber) { | ||
| Object userActiveSessions = objectOperRedisTemplate.opsForHash() | ||
| .get(RedisKeys.channelActive(channelId), userId); | ||
|
|
||
| if (userActiveSessions != null) { | ||
| Set<String> sessions = convertToSet(userActiveSessions); | ||
| sessions.forEach(session -> | ||
| activeSessions.add("\"" + session + "\"")); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. redis가 string인데 해당처럼 변환을 해줘야할까요?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
따라서 두 값의 형식 차이로 인해 Set.removeAll() 연산이 정상 동작하지 않았기 때문에 변환을 했습니다!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아아 이해했습니다! 피알 내용이 이부분인지 이해를 못했었네요 고생하셨어요! |
||
| } | ||
| } | ||
|
|
||
| return activeSessions; | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| private Set<String> convertToSet(Object storedValue) { | ||
| if (storedValue instanceof Set) { | ||
| return (Set<String>) storedValue; | ||
| } else if (storedValue instanceof List) { | ||
| return new HashSet<>((List<String>) storedValue); | ||
| } else { | ||
| return new HashSet<>(); | ||
| } | ||
| } | ||
| } | ||
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.
아래에서 mapper를 사용하시던데 그렇다면 gson을 사용하시는 부분 있으신가요?
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.
Kafka Producer에서 사용하고 있습니다!
Claude가 objectMapper와의 차이점을 다음과 같이 알려줬습니다!
Kafka Produce 시에는 Json 데이터를 단순히 String으로 변환만 하기 때문에 gson도 괜찮아 보여서 계속 사용하고 있었습니다!