Redis PUB/SUB, WebSocket 실시간 채팅 개발기 4 : 채팅방 리스트 최신화 성능 개선기
🔗 Redis PUB/SUB, WebSocket 실시간 채팅 개발기 1 : 설계하기
🔗 Redis PUB/SUB, WebSocket 실시간 채팅 개발기 2 : 채팅 기능 구현
🔗 Redis PUB/SUB, WebSocket 실시간 채팅 개발기 3 : 채팅방 리스트는 어떻게 최신화할까?
안녕하세요.
이번 포스팅에서는 채팅 기능을 어떻게 발전시켰는지 작성해 보겠습니다.
결과부터 보여드리자면, 저희의 채팅 기능 속도는 평균적으로 130ms -> 8ms 로 향상했습니다.
약 1500% 정도나 향상하였습니다. %로 하니까 숫자가 믿기지 않네요!
[캐치룸]은 채팅서버와 메인서버가 분리된 간단한 MSA 구조를 갖고 있습니다.
채팅 메시지는 Mongo DB에 저장되어, 채팅 서버에 연결되어 있는 반면에,
채팅방 리스트 정보는 회원과 상품 정보와 관계를 맺기 때문에 메인 서버에 연결된 RDS 안에 있는 MySQL에 테이블로 저장되어 있습니다.
위의 이미지는 캐치룸의 ERD 중 회원(user)과 상품(product)에 연관된 채팅방 정보 (chat_room) 테이블 부분입니다.
📌 Before : Feign Client 이용한 채팅방 리스트
위의 아키텍처처럼 채팅방 리스트는 Main에 연결된 RDS에 저장되어 있습니다.
그래서 채팅방 리스트가 프론트엔드까지 전달되는 과정은 다음과 같습니다.
- Chat 서버는 Feign Client로 Main 서버에 있는 채팅방 리스트를 조회하는 API 호출.
- Main 서버는 RDS에 접근하여 채팅방 정보를 조회 및 전달.
- Chat 서버는 받은 Feign Client를 가공하여 Front에 전달.
여기서 Feign Client가 걸리는 시간은 Test를 통해 확인해 보았습니다.
위 메서드는 Main 서버에 있는 채팅방 리스트를 가져오는 메서드입니다.
10번 연속으로 불러봤습니다. 평균적으로 100ms ~ 200ms 사이의 시간을 유지합니다.
사실 이 시간도 굉장히 빠른 시간입니다.
하지만 저희의 채팅방 리스트는 다음과 같은 방식으로 불러집니다.
채팅방 리스트는 채팅 메시지가 발행되면, 채팅 메시지와 함께 채팅방 리스트 또한 채팅을 하고 있는 사용자에게 발행됩니다.
즉 동시에 발행되는 구조입니다.
사용자는 채팅 화면에 들어가면 매우 빈번하게 채팅 메시지를 서버에 보낼 것입니다.
그렇다면 채팅방 리스트는 Main Server에 저장되어 있으니, Chat Server는 매번 Feign Client를 부르게 됩니다.
✏️ 매번 Feign Client를 호출할 경우 다음과 같은 문제점이 발생합니다.
- Main Server에 너무 많은 호출을 보내게 됩니다. Main Server는 MySQL에 잦은 조회로 인해 과도한 쿼리를 발생시키며, 이는 CPU와 메모리 부하 및 다른 클라이언트 요청에 영향이 끼칠 수 있습니다.
- 위의 영향으로 인해 문제가 생긴 Main Server로 인하여, Chat Server 또한 채팅 성능에 문제점이 발생할 수 있습니다.
- 현재는 100~200ms이지만, 더 많은 사용자가 몰릴수록 시간은 훨씬 느려질 수 있습니다.
캐치룸 채팅 개발팀은 위의 문제를 해결하고자, 🎒 Redis를 이용하기로 했습니다.
📌 After : Redis 이용한 채팅방 리스트
🌟 Redis?
key-value로 저장되는 NoSql 중 하나이자 In-Memory database입니다. 일반적인 RDBMS보다 읽기/쓰기에 빠른 속도를 가지고 있습니다. 저희의 채팅은 Redis의 pub/sub를 사용하고 있기 때문에, 마찬가지로 인메모리 데이터베이스로 Redis를 사용하기로 했습니다.
✏️ RedisConfig
Redis에 채팅방 리스트 객체를 저장하기 위해서 RedisConfig에 다음과 같은 메서드를 추가하였습니다.
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
return redisTemplate;
}
}
✏️ ChatRoomRedisRepository
채팅방 리스트를 Redis에 읽고 쓰고 하는 메서드는 다음과 같이 작성하였습니다.
- user 한 명당 채팅방 리스트를 Redis에 저장하도록 하였습니다.
- Hash 형태로 저장하여 각각의 채팅방 정보도 꺼낼 수 있도록 하였습니다.
@Repository
@RequiredArgsConstructor
public class ChatRoomRedisRepository {
private static final String CHAT_ROOM_KEY = "_CHAT_ROOM_RESPONSE_LIST";
private final RedisTemplate<String, Object> redisTemplate;
private final ObjectMapper objectMapper;
@Resource(name = "redisTemplate")
private HashOperations<String, String, ChatRoomListGetResponse> opsHashChatRoom;
private String getChatRoomKey(Long userId) {
// redis에 채팅방 리스트 저장 시 사용될 key
return userId + CHAT_ROOM_KEY;
}
public void initChatRoomList(Long userId, List<ChatRoomListGetResponse> list) {
// 채팅방 리스트 초기화
if (redisTemplate.hasKey(getChatRoomKey(userId))) {
redisTemplate.delete(getChatRoomKey(userId));
}
opsHashChatRoom = redisTemplate.opsForHash();
for (ChatRoomListGetResponse chatRoomListGetRes : list) {
setChatRoom(userId, chatRoomListGetRes.getChatRoomNumber(), chatRoomListGetRes);
}
}
public List<ChatRoomListGetResponse> getChatRoomList(Long userId) {
// 채팅방 리스트 조회
return objectMapper.convertValue(opsHashChatRoom.values(getChatRoomKey(userId)), new TypeReference<>() {});
}
}
Redis에서 값을 꺼내올 때 걸리는 시간입니다. 위의 Feign Clinet Test와 똑같이 테스트 코드를 작성했습니다.
위의 Feign Client를 10번 호출해 줬을 때보다 훨씬 시간이 단축된 상황입니다.
🌟 채팅방 리스트 초기화?
[캐치룸]의 채팅 기능 시퀀스 다이어그램입니다.
채팅방 초기화는 사용자가 채팅 기능에 진입할 때 진행됩니다.
ChatRoomListGetResponse에는 사용자 닉네임, 상대방 닉네임, 상품 정보도 저장되어 있기 때문에 적어도 하루에 한 번은 초기화가 이루어져야 한다고 생각되었습니다.
그래서 사용자가 채팅 기능에 접근할 때 Redis에 리스트를 초기화하도록 로직을 수정하였습니다.
📌 마무리
결론적으로 채팅방 리스트를 Redis에 적재시키면서 성능을 높였고, Main Server에도 영향이 가지 않도록 하였습니다.
리팩토링을 진행하면서 Redis에 대해 더 많이 알아갈 수 있었던 기간이었습니다.
이로써 채팅 개발기 글을 마칩니다.
읽어주시는 분들이 계실까 싶지만, 부족한 글들을 읽어주셔서 정말 감사합니다!