
- 영화 도서의 통합 검색 및 사용자 맞춤 데이터 제공
- 실시간 데이터 처리를 활용한 유저 간 상호작용
- 개발 인원: 백엔드 개발자 4명 (FE, BE 동시 개발)
- 프로젝트 기간:
2024.12.17 ~ 2025.02.13
(58일)
- 최초 DM 전송 시 프로필 페이지에서 채팅방 생성

- 일대일 채팅방 접속 중인 상태에서 상대방이 프로필 사진이 변경되면, 실시간 업데이트

- 일대일 채팅을 받으면, 그 받은 유저에게 알림이 오는 기능

Movlit 서비스에서는한 컨텐츠당 하나의 그룹 채팅방만 생성되도록 구현되어 있습니다.
인기 영화나 책이 공개되자마자 수많은 사용자가 동시에 “채팅방 만들기” 버튼을 누를 경우, 서버가 요청을 감당하지 못해 여러 채팅방이 생성될 수 있습니다.
이를 해결하기 위해Worker 클래스를 도입하였습니다.
Worker 클래스는Redis Queue와Thread Pool을 활용하여 채팅방 생성 요청을비동기적으로 처리합니다.
- Callable 인터페이스 활용
- 비동기 작업을 정의하여 Redis에서 데이터를 가져오는 작업을 처리합니다.
- Redis Queue Key 생성
- Key Prefix와 contentId를 조합하여 Redis Queue의 Key를 생성합니다.
- 데이터 조회
rightPop()
메서드를 사용하여 가장 오래된 생성 요청을 최대 10초 동안 대기하며 가져옵니다.- 만약 데이터가 없거나 의도한 String 형태가 아니라면 empty를 반환합니다.
- 정상적인 String 데이터라면 contentId와 memberId를 Map 형태로 반환합니다.
- Future를 통한 결과 처리
- ThreadPoolExecutor의
submit()
메서드를 통해 Callable task를 비동기 실행하고, Future 객체로 결과를 관리합니다. future.get(30초)
를 호출하여 작업 완료를 기다리며, 30초 내에 완료되지 않으면 TimeoutException이 발생합니다.- 예외 처리:
- **InterruptedException:**스레드가 인터럽트될 경우 현재 스레드의 인터럽트 상태를 재설정합니다.
- **ExecutionException:**Callable 실행 중 예외 발생 시 처리합니다.
- **TimeoutException:**작업이 30초 안에 완료되지 않으면 발생합니다.
- 위 세 가지 예외 발생 시,
GroupChatroomCreationWhenWorkingException
을 발생시켜 채팅방 생성 요청 처리 중 문제가 있음을 알립니다.
- ThreadPoolExecutor의


실시간 채팅 메시지 전송을 위해WebSocket과Redis Pub/Sub를 활용하고 있습니다.
- 메시지 전송 과정:
- 클라이언트가 채팅 메시지를 전송합니다.
- 서버는 메시지를 가공한 후, Redis Broker에 특정 토픽으로 발행합니다.
- 사용하는 토픽:
- 메시지 전송, 채팅방 업데이트, 채팅방 생성, 사용자 알림 등
- 후처리:
- Redis Subscriber가 각 토픽을 구독하여 후처리를 진행하고, 브로드캐스트 방식으로 실시간 반영됩니다.

채팅 메시지의실시간 전송과영구 저장을 위해 Redis Stream과 Pub/Sub를 사용합니다.
- 메시지 전송 및 처리 과정:
- 사용자가 채팅 메시지를 전송하면, 서버에서 메시지를 가공합니다.
- Redis Publish를 통해 다른 소비자에게 메시지를 전달하여 WebSocket과 SSE를 통한 UI 업데이트가 이루어집니다.
- 동시에 Redis Stream에 메시지를 추가하여, 하나 이상의 Consumer가 그룹으로 묶여 비동기적으로 처리합니다.
- MongoDB에 저장 후, ACK를 전송하여 메시지 처리가 완료되었음을 확인합니다.
- Redis Stream 사용 이유:
- **Consumer Group 지원:**Producer가 발행한 메시지를 여러 Consumer가 중복 없이 순차적으로 병렬 처리할 수 있습니다.
- **비동기 처리:**메시지 저장을 별도의 리스너가 비동기적으로 처리하여 수평 확장이 용이합니다.
- **실시간성과 안정성 분리:**메시지 전송/알림은 실시간, 저장은 비동기 처리함으로써 효율적인 파이프라인을 구축합니다.



- Consumer 클래스:
- 별도의 Consumer 클래스를 정의하여 Consumer Group의 등록, 구독 설정, 종료 처리를 관리합니다.
@PostConstruct
로 애플리케이션 시작 시 ConsumerGroup이 없으면 생성하고, 지정한 그룹과 이름으로 StreamOffset 이후의 메시지를 구독합니다.
- StreamListener 구현체 (ChatMessageStreamListener):
- Redis 스트림에서 전달된 채팅 메시지를 처리하며, MongoDB에 저장 후 ACK를 전송합니다.
그룹 채팅방에 참여한 멤버 정보를 실시간으로 조회할 경우 DB 부하가 크므로Redis 캐싱을 도입하였습니다.
- 처리 과정:
- **Cache Hit:**Redis에 캐싱된 데이터를 빠르게 조회합니다.
- **Cache Miss:**DB에서 조회 후 Redis에 캐싱하여 이후 요청에 빠르게 응답할 수 있도록 합니다.

일대일 채팅방은 삭제 없이 새로운 채팅이 추가되는 형태이므로 변경 빈도가 낮습니다.
따라서, 채팅방 목록 역시 Redis 캐싱을 통해 빠르게 조회할 수 있도록 처리하였습니다.
- 처리 과정:
- **Cache Hit:**Redis에 저장된 캐시 데이터를 조회합니다.
- **Cache Miss:**DB에서 데이터를 가져온 후 Redis에 캐싱합니다.

일대일 채팅의 경우,
- 상대방에게 최초 메시지 전송 시 일대일 채팅방이 생성되고 메시지가 전송됩니다.
- RDB에 채팅방 정보 저장 후 Redis 캐시를 업데이트합니다.
- 이후 토픽을 발행하여 Redis Subscriber 클라이언트를 통해 채팅방 및 메시지 정보를 전달합니다.

채팅 중에 사용자의 프로필 사진 등 멤버 정보가 변경되면, 이를 실시간으로 반영할 필요가 있습니다.
- 처리 과정:
- 프로필 업데이트 후 이벤트를 발행합니다.
- 업데이트된 멤버 정보를 기반으로 Redis 캐시를 갱신합니다.
- Redis로 Publish하여, Subscriber가 WebSocket에 연결된 클라이언트에게 변경된 정보를 전송합니다.

업데이트된 멤버 정보를 캐시에 반영하는 과정은 다음과 같이 진행됩니다.
- 처리 과정:
- Redis에 저장된 기존 멤버 목록을 불러옵니다. (캐시가 없다면 DB에서 조회)
- 이벤트에서 전달받은 업데이트된 멤버 객체를 생성합니다.
modifyCachedMember
함수를 통해 캐시된 멤버 목록에서 업데이트된 멤버와 ID가 같은 항목을 찾아 갱신합니다.updateCachedMembers
함수를 통해 변경된 전체 멤버 목록을 Redis에 다시 저장하고, 변경사항을 Publish합니다.



사용자가 특정 이벤트 발생 시즉각적인알림을 받을 수 있도록 SSE(Server-Sent Events)를 사용하여 브라우저 알림과 페이지 내 알림(알림 리스트)을 제공합니다.
- 알림 적용 대상:
- 새로운 팔로워 추가
- 사용자가 찜한 콘텐츠의 그룹 채팅방 생성
- 1:1 채팅 메시지 수신
- 그룹 채팅 메시지 수신
- 처리 과정:
- 클라이언트가 SSE 연결 요청 시
addEmitter
메서드가 호출되어 새로운 SSE 연결이 생성됩니다. - 30초 간격으로 하트비트(keep-alive)를 전송하여 연결을 유지합니다.
- 클라이언트가 SSE 연결 요청 시
- 브라우저 알림:

- 알림 리스트:

SSE는 단일 서버 인스턴스에 연결된 클라이언트에게만 알림을 전송할 수 있는 한계가 있습니다.
다중 서버(로드밸런싱) 환경에서는 각 서버가 자신이 관리하는 SSE 연결에만 알림을 보낼 수 있습니다.
따라서Redis Pub/Sub을 사용하여,
- 모든 서버 인스턴스가 특정 채널을 구독하고,
- 발행된 메시지를 각 서버가 받아 자신이 관리하는 SSE 연결을 통해 클라이언트에게 알림을 전달할 수 있도록 하였습니다.

Frontend | ||
React (Vite) | 사용자 인터페이스 구축을 위한 프론트엔드 라이브러리 (Javascript) | |
Backend | ||
Springboot 3.4 | 웹 애플리케이션 백엔드 프레임워크 (Java) | |
Spring Data JPA | JPA기반의 리포지토리를 통한 데이터 액세스 수행 | |
Spring Security | Spring Security, OAuth2.0, JWT를 활용한 인증, 권한 관리 | |
Test & Doc | ||
Rest Assured 5.3 | REST API 테스트를 수행 | |
Spring REST Docs 3.0 | API 문서를 자동으로 생성 | |
DB & Caching | ||
MySQL 8.0 | 유저정보, 콘텐츠 정보, 채팅방 정보 등의 정형 데이터 | |
MongoDB 5.0 | 채팅 메시지, 사용자 push알림과 같은 비정형 데이터 | |
Elasticsearch 8.16 | 검색 및 추천을 위한 콘텐츠 데이터 저장소 | |
Redis 7.0 | caching, pub/sub, queue, stream 사용 | |
Build & Deploy | ||
Github Actions | CI/CD 자동화를 위해 GitHub에서 코드 변경 관리 및 빌드, AWS 환경에 배포 | |
AWS | AWS EC2 및 RDS, ELB, Route53, ACM을 활용해 배포 환경 구성 | |
Docker | Vite-React, Springboot, Redis의 Docker Container 환경 구축 |
팀장 | 팀원 | 팀원 | 팀원 |
---|---|---|---|
정원준 | 민윤기 | 허지원 | 김민지 |