SSAFY) 실시간 채팅 구축 1 - 설계 및 RDB

GoRuth·2025년 5월 30일

ssafy - 채팅

목록 보기
1/2

이 시리즈는 채팅 시스템을 구축하면서 겪은 경험을 정리한 기술 회고로,
해당 글은 채팅에서 사용한 기술을 선택한 이유와 MVP를 위해 가장 먼저 구축했던 RDB 기반 채팅 시스템에 대해 다룹니다.


채팅 개발한 이유

팀 프로젝트를 진행하면서, AI 기반 전자기기 중고거래 플랫폼을 개발하게 되었습니다. 저는 백엔드를 담당하며, 채팅파트를 맡아 진행했습니다.

이에, 가장 먼저 사용할 기술에 대해 고려했고, 여러 후보 중 STOMP와 Redis를 선택했습니다.

STOMP & Redis를 선택한 이유!

  • STOMP
    • 기획단계에서 단체방에 대한 고려 ⭕ -> 메시지 라우팅이 자동으로 가능한 구조가 필요했습니다.
      • @MessageMapping + @SendTo을 통해 편하게 구축가능!
    • 읽음 유무 판단, 마지막 메세지 제공 같은 실제 채팅 서비스를 목표로 진행했고, 구현 과정에서의 최대한 리소스를 줄일 필요가 있었습니다.
      • 메시지 타입 분기, 목적지 라우팅, 세션/유저 추적 등 다양한 기능을 기본 제공하여 직접 구현해야 할 로직이 최소화!
  • Redis
    • 6주 안에 채팅에 읽은 시간, 채팅 메세지 캐싱 등을 도입 및 실제 사용이 가능한 완성도가 나와야 했기에, 러닝커브가 적어야 했음.
      • MongoDB가 가장 많은 레퍼런스를 가지고 있지만, 채팅 이외의 작업도 많았던 상황에서 학습을 진행하기에는 무리라는 생각에 제외
      • 가장 많이 사용해보고 익숙한 NoSQL이 Redis였음
    • 채팅에서 가장 중요한 건 속도라고 생각
      • Redis의 ZSet을 통해 score를 만들어 구축하여, 채팅을 순차적으로 저장!
      • In-Memory 구조로 NoSQL 중에서 가장 빠른 속도를 가짐!

요구사항

기획단계에서 나왔던 요구사항이 다음과 같이 나왔고, 이에 맞춰서 진행했습니다.

1. 메시지 실시간 송수신
2. 채팅방 단위로 마지막 메세지를 보여주기
3. 각 사용자별로 읽은 메시지 정보 제공
4. 실시간으로 읽음여부를 파악

로직


고려사항

  • 메시지는 chat_message 테이블에 시간 순으로 쌓입니다.
  • read_point는 사용자가 마지막으로 읽은 메시지를 기준으로 읽음 여부를 계산하는 데 사용됩니다.
  • 채팅방 생성 후, 초기 데이터가 없기에 해당 상황에서의 기본 return값 지정이 필요합니다.

🔨 기술 스택 및 구현

  • Java 17
  • Spring Boot 3.x
  • Spring Data JPA
  • MySQL 8.0

메시지는 WebSocket으로 수신되며, 바로 JPA를 통해 DB에 저장합니다.

  1. stomp 구성
@Slf4j
@Component
@RequiredArgsConstructor
public class StompChannelInterceptor implements ChannelInterceptor {
    private final Map<String, Set<String>> sessions = new ConcurrentHashMap<>();

    @Override
    public Message<?> preSend(@NonNull Message<?> message, @NonNull MessageChannel channel) {
        StompHeaderAccessor accessor =
                MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

        // 올바른 접근인 지 판단

        switch (accessor.getCommand()) {
            case CONNECT -> {
                handleConnect(accessor);
            }
            case SEND -> {
                handleSend(accessor, message);
            }
            case DISCONNECT -> {
                handleDisconnect(accessor);
            }
        }

        return message;
    }

    private void handleDisconnect(StompHeaderAccessor accessor) {
        //연결 종료 시, 행동할 메소드
    }

    private void handleSend(StompHeaderAccessor accessor, Message<?> message) {
        // 전송 시, RDB에 저장

    private void handleConnect(StompHeaderAccessor accessor) {
    	// 연결 시, jwt 검증 시도
    }
}

SSAFY 프로젝트로 아직 반출신청을 못해서...
지금은 큰 로직만 보여드리고, 추후에 자세한 코드를 첨부하겠습니다.

예상되는 한계

한계

  • 메시지가 많아질수록 쿼리 성능이 저하될 가능성이 매우 높습니다.
  • 실시간 요청을 하지만 RDB에 저장/조회되는 과정이 Redis보다 상대적으로 느립니다.

트래픽이 올라가면서…

예상대로 Chatting 조회 테스트를 진행하면서, 메시지 건수를 늘렸더니 속도가 저하되었습니다. 그래서 1차적으로 Slice<>를 통한 필요한 데이터만 return하도록 구성했고, 캐시 구성을 위한 Redis 기반의 채팅으로 전환을 진행했습니다.

Next

RDB로 간단한 MVP를 구성했으니, 이제는 RDB에서 Redis로 전환하면서 속도를 높인 로직으로 바꿔보겠습니다.

profile
Backend Developer

0개의 댓글