유저 신고 기능을 위한 Redis 채팅 로그 관리
유저 신고를 위한 채팅 로그 관리
게임 내 실시간 채팅 기능을 제공하면서, 유저 신고 기능도 자연스럽게 필요해졌다.
단순히 채팅 한 줄만 저장해서 신고하는 방식은 정확한 맥락 파악이 어렵기 때문에, 신고된 메시지를 기준으로 앞뒤의 대화 흐름까지 함께 저장하는 구조가 필수적이었다.
이를 위해 나는 Redis를 활용한 채팅 로그 저장 시스템을 설계했고, 그 과정에서 여러 가지 현실적인 문제에 직면하게 되었다.
Redis TTL 방식의 한계
처음에는 Redis를 활용해 각 채팅 로그에 TTL을 설정하고, 일정 시간이 지나면 자동으로 삭제되도록 구현했다. 이 방식은 메모리 누수를 방지하고 효율적인 리소스 관리를 가능하게 해주는 좋은 방법이다.
하지만 직접 테스트하면서 아래와 같은 문제점들을 발견했다:
- •
시간 기반 TTL의 경우 신고 직전에 채팅 로그가 삭제되는 문제
예를 들어 누군가 불쾌한 채팅을 보낸 뒤 얼마 지나지 않아 TTL로 인해 로그가 삭제되면, 신고를 하려고 해도 근거가 사라지는 문제가 생긴다.
- •
수량 기반의 경우 채팅이 폭주할 때 필요한 채팅로그가 삭제되는 문제
게임 중에 채팅이 폭주하는 경우, 1시간 이내라도 굉장히 많은 채팅이 쌓일 수 있다. 이때 시간 기준 삭제는 필요한 로그가 사라질 가능성이 더 커진다.
그래서 이렇게 설계했다
채팅 로그 저장 방식
채팅 메시지는 Redis의 Sorted Set(ZSet) 구조를 활용해 저장된다. 각 메시지는 다음과 같은 방식으로 저장된다:
- •
Redis 키는 채팅 유형에 따라 다음과 같이 구분된다:
- ◦
전체 채팅:
chat:all:log
- ◦
방 채팅:
chat:room:{roomId}:log
- ◦
개인 채팅:
chat:private:{user1}:{user2}:log
(두 유저 ID를 정렬하여 키 일관성 유지)
- •
메시지는 직렬화(serialize)된 후,
sentAt
시점을 기준으로 score에 현재 timestamp(millisecond)를 부여하여 저장된다.
- •
이 구조 덕분에 시간순 정렬, 범위 기반 조회, 시간 기반 삭제 처리가 가능하다.
public void saveChatMessage(ChatType chatType, String identifier, ChatMessage message, String senderId) {
String key = chatKey(chatType, identifier, senderId);
String serialized = jsonSerializer.serialize(message, CHAT_ERROR);
double score = Instant.now().toEpochMilli();
redisTemplate.opsForZSet().add(key, serialized, score);
}
시간 기준 TTL 삭제는 유지하되, 정기적인 청소 방식으로 보완
나는 Redis의 TTL을 단순히 설정해놓고 자동 삭제되도록 내버려두기보다는, 내가 직접 삭제 시점을 컨트롤할 수 있도록 변경했다.
이를 위해 아래처럼 구성했다:
- •
ChatLogService
에서 Redis Sorted Set을 사용해 메시지를 저장 (score: timestamp)
- •
메시지 저장 시 TTL이 아닌 타임스탬프 기반으로 수동 정리
- •
@Scheduled
로 1분마다 만료 메시지 삭제 작업 실행
@Scheduled(fixedRate = 60000)
public void cleanExpiredChatLogs() {
// 전체 채팅 로그
chatLogService.removeExpiredMessages(ChatType.ALL, null);
// 방별 채팅 로그
Set<String> roomIds = roomRepository.getAllRoomIds();
for (String roomId : roomIds) {
chatLogService.removeExpiredMessages(ChatType.ROOM, roomId);
}
// 개인 채팅 로그
Set<String> privateKeys = chatLogService.getPrivateChatKeys();
for (String key : privateKeys) {
chatLogService.removeExpiredMessagesByKey(key);
}
}
이 방식의 장점은 다음과 같다:
- •
삭제 주기를 세밀하게 제어할 수 있음
- •
데이터가 필요할 때는 TTL과 상관없이 조회 가능
- •
Redis 자체 TTL보다 유연하게 동작
유저 신고 흐름
- 1
사용자가 유저를 신고하면, 해당 채팅의
sentAt
기준 앞뒤 10개 채팅 로그를 Redis에서 가져온다.
- 2
이 때
ChatLogService.getSurroundingMessages()
를 통해 문맥을 함께 저장한다.
- 3
이후 해당 데이터를 기반으로 신고 DB에 저장되고, 관리자 검토로 이어진다.
List<ChatMessage> chatLog = chatLogService.getSurroundingMessages(
reportReq.chatType(),
reportReq.identifier(),
userDetails.getUuid(),
reportReq.reportedChatTime(),
10
);
//신고한 채팅을 기준으로 앞뒤 10개의 채팅 가져오기
public List<ChatMessage> getSurroundingMessages(ChatType chatType, String identifier, String senderId, LocalDateTime baseTime, int range) {
String key = chatKey(chatType, identifier, senderId);
long baseTimestamp = baseTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
// 기준 메시지
Set<String> baseMessageRaw = redisTemplate.opsForZSet()
.rangeByScore(key, baseTimestamp, baseTimestamp);
// 앞쪽 메시지 (기준 이전 메시지들)
Set<String> beforeRaw = redisTemplate.opsForZSet()
.reverseRangeByScore(key, 0, baseTimestamp - 1, 0, range);
// 뒤쪽 메시지 (기준 이후 메시지들)
Set<String> afterRaw = redisTemplate.opsForZSet()
.rangeByScore(key, baseTimestamp + 1, Double.MAX_VALUE, 0, range);
// 병합 후 정렬 (총 20개)
List<String> combined = new ArrayList<>(beforeRaw);
combined.addAll(baseMessageRaw);
combined.addAll(afterRaw);
return combined.stream()
.map(raw -> jsonSerializer.deserialize(raw, ChatMessage.class, CHAT_ERROR))
.sorted(Comparator.comparing(ChatMessage::sentAt))
.collect(Collectors.toList());
}
이 방식 덕분에 한 줄짜리 채팅만으로 신고 여부를 판단하는 것이 아니라, 대화 흐름 전체를 고려할 수 있게 되었다.
그러나, 이 방식도 완벽하진 않다
현재 설계 방식은 기존 TTL 방식보다 유연하지만, 여전히 몇 가지 단점과 개선 여지가 있다.
1. Redis 메모리 사용 증가
- •
채팅 로그가 TTL 없이 Redis에 계속 쌓이기 때문에 활성 유저 수가 많거나 대화량이 많은 상황에서는 Redis 메모리를 빠르게 소비할 수 있다.
- •
정리 주기가 짧다 해도 고속으로 채팅이 쌓이면 처리 지연 혹은 메모리 부족 문제가 생길 가능성이 있다.
🔧 개선 방향
- •
채팅 로그에 대해 유저 단위, 방 단위의 최대 저장 개수 제한을 두고 초과 시 오래된 메시지부터 제거하는 방식 고려
- •
Redis 외에 ElasticSearch, Kafka + S3 등 로그 전용 스토리지로 이관하는 방안도 중장기적으로 검토할 수 있다.
2. 1분 주기의 정리 간격 문제
- •
정리 주기가 1분이기 때문에 정리 직전의 시점에 저장된 채팅은 여전히 최대 59초간 유지된다.
- •
엄밀히 말해 완전히 실시간 삭제는 아니기 때문에 민감한 이슈(욕설, 불법 채팅 등)에 대해 즉각 반응이 어려울 수 있다.
🔧 개선 방향
- •
중요도가 높은 채팅 로그에 한해 별도 채널로 실시간 필터링 + 로그 저장 처리
- •
신고 발생 시 해당 키에 대해 TTL을 일시 중지하거나 일정 시간 동안 강제로 보존하는 기능 도입
3. 채팅 키 수 증가로 인한 키 관리 부담
- •
chat:room:{roomId}:log
,chat:private:{user1}:{user2}:log
등으로 다양한 키가 생성되는데,활성 유저 수가 많을수록 Redis 키 수가 급증하고 이로 인해 성능 저하가 발생할 수 있다.
🔧 개선 방향
- •
Redis Cluster 환경으로 확장하거나 비활성 키를 주기적으로 모니터링 및 제거하는 유지 관리 루틴 추가
- •
비중이 낮은 채널은 저장 주기 또는 정리 빈도를 줄여 분산 처리
정리하며
신고 기능의 신뢰성과 사용성 확보를 위해 단순 TTL이 아닌 수동 삭제 기반의 정리 방식을 도입했다.
이를 통해 데이터 유실을 최소화하고 정확한 채팅 흐름 보존이 가능해졌다.
그러나 여전히 Redis 특성상 메모리 관리, 키 수 관리, 삭제 주기 최적화 등의 과제는 존재하며
이를 해결하기 위해 다음과 같은 방향으로 계속 개선해 나갈 계획이다:
- •
저장량 제한 및 우선순위 기반 보존 정책 도입
- •
로그 저장소 분리 (e.g., DB, 로그 전용 스토리지)
- •
정리 스케줄 최적화 및 키 정리 로직 개선
운영 환경에서의 경험을 통해 얻은 교훈들을 바탕으로 더 효율적이고 견고한 구조로 리팩토링해나갈 예정이다.