
이 글은 실시간 채팅 시스템을 구축하며 성능 최적화와 기술 스택 조합을 통해 대규모 트래픽을 안정적으로 처리한 경험을 공유하게 되었습니다.
저처럼 최적화하면서 고생하신 초보 개발자를 위해, 복잡한 개념을 쉽게 설명하고 구체적인 사례와 코드를 통해 실질적인 인사이트를 제공합니다.
실시간 채팅 시스템은 사용자 간 즉각적인 소통을 요구합니다.
초기에는 WebSocket으로 메시지를 주고받는 간단한 구조였지만, 사용자 수가 증가하면서 서버 부하가 급격히 커졌습니다.
주요 문제는 다음과 같았습니다.
이를 해결하기 위해 메시지 큐, 캐싱, 채널 관리 최적화를 통해 시스템을 재구성했습니다.
이 과정은 단순히 코드를 작성하는 것을 넘어, 실제 트래픽을 감당할 수 있는 아키텍처 설계로 이어졌습니다.
성능 문제를 해결하기 위해 세 가지 핵심 최적화를 진행했습니다.
동기 방식에서는 서버가 메시지 처리를 기다리며 리소스를 소모했습니다.
이를 해결하기 위해 Apache Kafka를 도입해 메시지를 비동기적으로 처리했습니다.
데이터베이스 조회로 인한 부하를 줄이기 위해 Redis를 활용해 사용자 세션 데이터를 캐싱했습니다.
Netty 기반 WebSocket 서버에서 채널 관리를 최적화했습니다.
사용자와 채널 간 매핑을 효율적으로 처리하기 위해 UserChannelSession 클래스를 설계했습니다.
이 프로젝트는 다양한 기술을 조합해 유연성과 안정성을 확보했습니다.
각 기술의 선택 이유와 역할을 간단히 정리했습니다.
| 기술 | 역할 | 선택 이유 |
|---|---|---|
| Spring Boot | 백엔드 프레임워크 | 모듈화와 의존성 주입으로 유지보수성 강화 |
| Kotlin | 일부 모듈 개발 | Null 안전성과 간결한 문법으로 안정성 확보 |
| Redis | 세션 관리, 캐싱 | 빠른 읽기/쓰기 성능으로 실시간 요구사항 충족 |
| MySQL + MongoDB | 데이터 저장 | 구조화/비정형 데이터 모두 효율적 처리 |
| Kafka | 메시지 큐 | 대량 메시지 처리와 확장성 제공 |
| Netty | WebSocket 서버 | 고성능 네트워크 처리로 동시 접속 지원 |
이러한 조합은 단순히 트렌드를 따르는 것이 아니라, 서비스 요구사항에 맞춘 최적의 선택이었습니다.
시스템을 실제 서비스에 배포하며 여러 이슈를 해결했습니다.
주요 사례는 다음과 같습니다.
최적화와 기술 적용의 결과는 다음과 같은 구체적인 지표로 나타났습니다.
이러한 결과는 기업이 신뢰할 수 있는 실무 성과를 보여줍니다.
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);
}
}
이 프로젝트에서 얻은 가장 큰 교훈은 문제 중심 접근과 실무적 최적화의 중요성입니다.
다음 세 가지를 추천합니다.
문제부터 파악하라: 기술을 배우기 전에 시스템이 해결해야 할 문제를 명확히 정의하세요.
작게 시작하라: Kafka나 Redis 같은 복잡한 기술은 작은 프로토타입으로 먼저 테스트해보세요.
운영을 경험하라: 실제 배포와 장애 대응을 통해 이론만으로는 알 수 없는 실무 감각을 키우세요.