cd ..

Realtime Chat

실시간 채팅에서 roomId 파티션, Redis Pub/Sub, Cache Aside를 적용해 순서를 지키며 REST RPS를 937→1,598로 끌어올렸습니다.

1,598 RPS
REST API

200 VU 기준 Cache Aside 적용 후 측정한 처리량

1,158
동시 세션

2대 스케일아웃 WebSocket 환경에서 유지한 연결 수

<1.5ms
주요 쿼리

EXPLAIN ANALYZE 기준 인덱스 최적화 후 조회 시간

Java 21Spring Boot 3Spring WebSocketSTOMPApache KafkaRedisPostgreSQLSpring Data JPAJWTDockerPrometheusGrafanaTestcontainersk6
Key Result

k6 REST API 부하테스트(200 VU): RPS 937 → 1,598 (+70.5%), p50 54ms → 16ms (-69.5%), 에러율 0%.

개요

실시간 채팅은 순서 보장, 다중 인스턴스 브로드캐스트, 장애 복구가 동시에 풀려야 하는 문제였습니다. WebSocket 메시지를 Kafka로 흘리고 roomId 파티션 키로 순서를 고정했으며, Redis Pub/Sub로 서버 간 브로드캐스트를 처리했습니다. REST API는 JPQL 프로젝션과 Cache Aside로 가볍게 만들고, WebSocket·Kafka 구간은 멱등성과 DLT까지 넣어 복구 가능성을 검증했습니다.

메시지 파이프라인

  • WebSocket(STOMP) → Kafka produce (partition key = roomId로 같은 방 순서 보장)
  • Consumer Group 1 (chat-persistence): DB 저장 + 멱등성 체크 (messageKey UUID + DB UK)
  • Consumer Group 2 (chat-broadcast): Redis Pub/Sub → 모든 서버 인스턴스에 브로드캐스트
  • Consumer Group 3 (chat-read-receipt): 읽음 처리 + unreadCount 갱신
  • 장애 복구: manual offset commit + 3회 재시도 → DLT(Dead Letter Topic) 격리

성능 최적화

  • N+1 쿼리: 채팅방 목록 2N+1회 → JPQL Constructor Expression 1회 쿼리
  • Redis Cache Aside: 채팅방 목록 캐싱 TTL 5분, 3가지 무효화 전략 (방 생성, 메시지 수신, 읽음 처리)
  • DB 인덱스: EXPLAIN ANALYZE 기반 5개 인덱스 설계, 모든 쿼리 < 1.5ms
  • k6 REST API: 200 VU, RPS 937 → 1,598 (+70%), p50 54ms → 16ms (-69%)
  • k6 WebSocket: 100 VU, 579 동시세션 (2대 스케일아웃 시 1,158 세션)

프로덕션 품질

  • Rate Limiting: STOMP ChannelInterceptor + 슬라이딩 윈도우 10msg/sec
  • 온라인/오프라인 상태: Redis presence TTL 60초 + WebSocket 이벤트 + Pub/Sub
  • Graceful Shutdown + Health Check (Spring Actuator)
  • Prometheus + Grafana 모니터링 (Micrometer 커스텀 메트릭 5개, 대시보드)
  • Testcontainers 통합 테스트 20개 (PostgreSQL, Kafka, Redis 자동 구동)

아키텍처

전체 아키텍처

ERD

전체 ERD

시퀀스 다이어그램

메시지 전송 & 브로드캐스트
읽음 처리
Consumer 장애 복구

문제 원인

  1. 01여러 서버 인스턴스에서 동시에 메시지를 발행하면 채팅방 내 메시지 순서가 깨질 수 있었습니다.
  2. 02서버를 2대로 스케일아웃하면 WebSocket 세션이 공유되지 않아 다른 서버의 클라이언트에게 메시지를 전달할 수 없었습니다.
  3. 03Consumer가 메시지 처리 중 실패하면 메시지가 유실되거나 중복 저장될 수 있었습니다.
  4. 04채팅방 목록 API에서 N+1 쿼리가 발생해 방 10개 기준 21회 쿼리가 실행되어 응답이 느렸습니다.

해결 과정

  1. 01Kafka partition key = roomId로 같은 방의 메시지를 동일 파티션에 할당해 순서를 보장했습니다.
  2. 02Kafka Consumer Group 2가 Redis Pub/Sub로 메시지를 발행하고, 모든 서버가 구독해 크로스 서버 브로드캐스트를 구현했습니다.
  3. 03manual offset commit + 멱등성(messageKey UUID + DB UK)으로 중복 저장을 방지하고, 3회 재시도 실패 시 DLT로 격리했습니다.
  4. 04JPQL Constructor Expression으로 DTO 프로젝션 단일 쿼리를 작성하고, Redis Cache Aside(TTL 5분)로 반복 조회 부하를 제거했습니다.

결과

  1. 01k6 REST API 부하테스트(200 VU): RPS 937 → 1,598 (+70.5%), p50 54ms → 16ms (-69.5%), 에러율 0%.
  2. 02k6 WebSocket 부하테스트(100 VU): 579 동시세션(단일), 1,158 동시세션(2대 스케일아웃), 연결 실패 0%.
  3. 03EXPLAIN ANALYZE: 모든 주요 쿼리 < 1.5ms, 5개 커스텀 인덱스로 Sequential Scan 제거.
  4. 04Consumer 장애 복구: 멱등성 + DLT 격리로 메시지 유실·중복 없이 처리. 통합 테스트 20개 전체 통과.