대규모 실시간 채팅 시스템 Kafka, Redis, Netty로 최적화 개선 학습하기

궁금하면 500원·2025년 4월 30일
0

미생의 개발 이야기

목록 보기
40/58

실시간 채팅 시스템 구축과 최적화

이 글은 실시간 채팅 시스템을 구축하며 성능 최적화와 기술 스택 조합을 통해 대규모 트래픽을 안정적으로 처리한 경험을 공유하게 되었습니다.

저처럼 최적화하면서 고생하신 초보 개발자를 위해, 복잡한 개념을 쉽게 설명하고 구체적인 사례와 코드를 통해 실질적인 인사이트를 제공합니다.

프로젝트 배경: 왜 최적화가 필요했나?

실시간 채팅 시스템은 사용자 간 즉각적인 소통을 요구합니다.
초기에는 WebSocket으로 메시지를 주고받는 간단한 구조였지만, 사용자 수가 증가하면서 서버 부하가 급격히 커졌습니다.

주요 문제는 다음과 같았습니다.

  • 높은 응답 지연: 동기 방식으로 메시지를 처리하며 서버 리소스가 낭비되었습니다.
  • 데이터베이스 부하: 빈번한 조회로 인해 응답 속도가 저하되었습니다.
  • 동시 접속 한계: 수백 명 이상의 동시 접속 시 서버가 불안정해졌습니다.

이를 해결하기 위해 메시지 큐, 캐싱, 채널 관리 최적화를 통해 시스템을 재구성했습니다.
이 과정은 단순히 코드를 작성하는 것을 넘어, 실제 트래픽을 감당할 수 있는 아키텍처 설계로 이어졌습니다.

성능최적화 튜닝

성능 문제를 해결하기 위해 세 가지 핵심 최적화를 진행했습니다.

1.Kafka로 비동기 메시지 큐 도입

동기 방식에서는 서버가 메시지 처리를 기다리며 리소스를 소모했습니다.
이를 해결하기 위해 Apache Kafka를 도입해 메시지를 비동기적으로 처리했습니다.

  • 구현 방법: 메시지를 Kafka 토픽에 저장한 후, 소비자가 비동기로 처리.
  • 효과: 메시지 전송 시간이 평균 200ms에서 50ms로 약 75% 단축.
  • 교훈: 비동기 처리는 서버 부하를 줄이고 확장성을 높이는 강력한 방법입니다.

2. Redis로 세션 데이터 캐싱

데이터베이스 조회로 인한 부하를 줄이기 위해 Redis를 활용해 사용자 세션 데이터를 캐싱했습니다.

  • 구현 방법: 자주 접근하는 세션 데이터를 Redis에 저장, 1시간 TTL 설정.
  • 효과: 데이터베이스 조회가 80% 감소, 응답 속도 2배 개선.
  • 교훈: 캐싱은 실시간 시스템에서 빠른 응답을 보장하는 필수 전략입니다.

3. Netty로 효율적인 채널 관리

Netty 기반 WebSocket 서버에서 채널 관리를 최적화했습니다.
사용자와 채널 간 매핑을 효율적으로 처리하기 위해 UserChannelSession 클래스를 설계했습니다.

  • 구현 방법: ConcurrentHashMap을 사용해 사용자 ID와 채널 목록을 매핑.
  • 효과: 동시 접속 1,000명에서 5,000명 이상으로 처리 가능.
  • 교훈: 고성능 네트워크 처리는 대규모 트래픽에서 안정성을 보장합니다.

최신과 검증된 기술의 조화

이 프로젝트는 다양한 기술을 조합해 유연성과 안정성을 확보했습니다.
각 기술의 선택 이유와 역할을 간단히 정리했습니다.

기술역할선택 이유
Spring Boot백엔드 프레임워크모듈화와 의존성 주입으로 유지보수성 강화
Kotlin일부 모듈 개발Null 안전성과 간결한 문법으로 안정성 확보
Redis세션 관리, 캐싱빠른 읽기/쓰기 성능으로 실시간 요구사항 충족
MySQL + MongoDB데이터 저장구조화/비정형 데이터 모두 효율적 처리
Kafka메시지 큐대량 메시지 처리와 확장성 제공
NettyWebSocket 서버고성능 네트워크 처리로 동시 접속 지원

이러한 조합은 단순히 트렌드를 따르는 것이 아니라, 서비스 요구사항에 맞춘 최적의 선택이었습니다.

장애 대응과 모니터링 실제 운영

시스템을 실제 서비스에 배포하며 여러 이슈를 해결했습니다.
주요 사례는 다음과 같습니다.

  • Kafka 브로커 장애: 메시지 유실 방지를 위해 복제 팩터를 3으로 설정하고 Ack를 all로 조정. 신뢰성 99.9% 이상 확보.
  • 성능 모니터링: Prometheus와 Grafana로 CPU 사용률과 응답 지연을 실시간 모니터링. 문제 발생 시 즉시 대응 가능.
  • 로그 관리: ELK 스택으로 로그를 중앙화해 장애 원인을 신속히 파악.
  • 교훈: 운영 경험은 개발뿐 아니라 시스템 안정화와 장애 대응 능력을 키웁니다.

숫자로 입증된 성과 결과

최적화와 기술 적용의 결과는 다음과 같은 구체적인 지표로 나타났습니다.

  • 응답 시간: 평균 200ms → 50ms (75% 단축).
  • 서버 리소스: CPU 사용률 70% → 30%, 메모리 사용량 50% 감소.
  • 동시 접속: 1,000명 → 5,000명 이상 안정적 처리.

이러한 결과는 기업이 신뢰할 수 있는 실무 성과를 보여줍니다.

public class ChatService {
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    // Kafka 비동기 메시지 전송
    public void sendMessage(String topic, String message) {
        kafkaTemplate.send(topic, message);
    }

    // Redis 세션 저장
    public void saveSession(String userId, String sessionData) {
        redisTemplate.opsForValue().set(userId, sessionData, 1, TimeUnit.HOURS);
    }

    // WebSocket 메시지 처리
    public void handleTextMessage(String senderId, String receiverId, String message, Channel currentChannel) {
        ChatMsg chatMsg = new ChatMsg(senderId, receiverId, message);
        DataContent dataContent = new DataContent();
        List<Channel> receiverChannels = UserChannelSession.getMultiChannels(receiverId);

        if (receiverChannels == null || receiverChannels.isEmpty()) {
            chatMsg.setIsReceiverOnLine(false);
        } else {
            chatMsg.setIsReceiverOnLine(true);
            for (Channel receiverChannel : receiverChannels) {
                Channel findChannel = clients.find(receiverChannel.id());
                if (findChannel != null) {
                    dataContent.setChatMsg(chatMsg);
                    String chatTimeFormat = LocalDateUtils.format(chatMsg.getChatTime(), LocalDateUtils.DATETIME_PATTERN_2);
                    dataContent.setChatTime(chatTimeFormat);
                    findChannel.writeAndFlush(new TextWebSocketFrame(JsonUtils.objectToJson(dataContent)));
                }
            }
        }
    }
}

public class UserChannelSession {
    private static final Map<String, List<Channel>> userChannels = new ConcurrentHashMap<>();
    private static final Map<String, String> channelUserRelation = new ConcurrentHashMap<>();

    public static void putMultiChannels(String userId, Channel channel) {
        userChannels.computeIfAbsent(userId, k -> new ArrayList<>()).add(channel);
    }

    public static void putUserChannelIdRelation(String channelId, String userId) {
        channelUserRelation.put(channelId, userId);
    }

    public static List<Channel> getMultiChannels(String userId) {
        return userChannels.get(userId);
    }
}

코드 설명

  • sendMessage: Kafka로 메시지를 비동기 전송.
  • saveSession: Redis에 세션 데이터를 저장해 빠른 조회 지원.
  • handleTextMessage: WebSocket 메시지 처리 로직. 수신자 채널을 확인해 메시지 전송.
  • UserChannelSession: 사용자와 채널 간 효율적인 매핑 관리.

결과

이 프로젝트에서 얻은 가장 큰 교훈은 문제 중심 접근과 실무적 최적화의 중요성입니다.
다음 세 가지를 추천합니다.

  1. 문제부터 파악하라: 기술을 배우기 전에 시스템이 해결해야 할 문제를 명확히 정의하세요.

    • 예: "응답 시간이 느리다" → "비동기 처리가 필요하다."
  2. 작게 시작하라: Kafka나 Redis 같은 복잡한 기술은 작은 프로토타입으로 먼저 테스트해보세요.

  3. 운영을 경험하라: 실제 배포와 장애 대응을 통해 이론만으로는 알 수 없는 실무 감각을 키우세요.

profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글