채팅 시스템 고도화 PRD

Background

스포츠 앱은 예약(booking), 중고 거래(goods), 티켓팅(ticketing), 게시판(post) 등 여러 도메인을 갖고 있지만, 채팅은 message 도메인 하나로 단순 1:1(DIRECT)·그룹(GROUP) 방 생성과 REST 기반 메시지 목록 조회만 지원합니다(Room.kt, MessageDomainService.kt). 실시간 전송 계층이 없고, 동아리·모임 같은 커뮤니티 개념도 없으며, 방 외부 초대(게스트) 기능도 없습니다.

앞으로 사용자가 스포츠 동아리·모임을 만들고 그 안에서 실시간으로 소통하려면 커뮤니티 도메인과 실시간 채팅이 필요하고, 모임에 속하지 않은 지인을 특정 대화에만 임시로 참여시키는 게스트 초대도 필요합니다. 또한 채팅이 커뮤니티 전용 기능으로 고립되지 않도록, 다른 도메인(중고 거래 등)도 같은 채팅 인프라를 재사용할 수 있는 연결 구조를 이번에 함께 정의합니다.

Problem Definition

AS-IS (코드 근거)

  • 채팅 도메인에는 실시간 전송 계층이 없다. 단 MCP 모듈이 SSE를 사용 중이라 Tomcat이 이미 장기연결에 민감하게 설정돼 있다: message 도메인(presentation·infrastructure)에는 WebSocket/STOMP 코드가 0건이라 클라이언트는 REST로 방 목록·메시지를 폴링해야 하므로 지연·배터리 소모 문제가 있습니다. 다만 채팅과 무관한 MCP(Model Context Protocol) 서버 모듈이 이미 SSE(Server-Sent Events) 트랜스포트를 사용 중입니다(application.yml:27-28, sse-endpoint: /mcp/sse, sse-message-endpoint: /mcp/message). 이 SSE 장기연결이 동기 서블릿 방식(SseEmitter)이라 연결 1개당 스레드 1개를 점유하는 문제를 이미 인지하고 Tomcat 스레드풀을 max: 400(MCP_TOMCAT_MAX_THREADS)으로 튜닝해둔 상태입니다(application.yml:4-11). 즉 새 WebSocket 연결도 같은 Tomcat 스레드풀을 공유하게 되므로 실시간 전송 계층을 “0에서 시작”하는 것이 아니라 “이미 장기연결 부하가 걸려 있는 서버에 추가”하는 문제로 다뤄야 합니다.
  • 읽음 확인·타이핑 표시·안읽은 수 없음: RoomParticipant.ktjoinedAt만 갖고 마지막으로 읽은 메시지 시점을 기록하지 않습니다. Room.kt도 참여자별 unread 카운트를 노출하지 않습니다.
  • 커뮤니티 도메인 없음: domain/ 하위는 booking, common, facility, goods, mcp, message, notification, operator, payment, post, ticketing, user, weather로 구성되어 있고 동아리·모임을 표현하는 도메인이 존재하지 않습니다. 모임을 만들고 그 모임 전용 채팅을 연결할 방법이 없습니다.
  • 게스트 초대 없음: 관련 코드가 0건입니다. MessageDomainService.kt#joinRoom은 임의 userId를 즉시 참여자로 추가할 뿐 초대·수락·권한 제한·만료 개념이 없습니다.
  • Room이 외부 엔티티에 연결되지 않음: Room 엔티티는 type(DIRECT/GROUP)과 name만 갖고 있어 특정 커뮤니티·주문·예약 같은 외부 컨텍스트와 구조적으로 연결할 필드가 없습니다. 향후 각 도메인이 채팅이 필요할 때마다 자체 방식을 중복 구현할 위험이 있습니다.

TO-BE

  • WebSocket/STOMP 기반 실시간 송수신, 읽음 확인, 타이핑 표시, 안읽은 수 배지를 갖춘 채팅으로 고도화합니다.
  • 커뮤니티(동아리·모임) 도메인을 신설해 개설·가입·멤버십·역할을 지원하고, 커뮤니티 전용 그룹 채팅과 자동 연동합니다.
  • 정회원을 특정 방에 한시적으로 초대하는 게스트 모델을 도입해 권한이 제한된 임시 참여를 지원합니다.
  • Room에 contextType/contextId를 도입해 외부 도메인이 채팅을 재사용할 수 있는 일반 확장 메커니즘을 만들고, 커뮤니티 그룹 채팅을 첫 번째 연동 사례로, goods 거래 채팅을 두 번째 연동 사례로 확정합니다.

Goals / Non-Goals

Goals

  • WebSocket/STOMP 실시간 송수신, 읽음 확인, 타이핑 표시, 안읽은 수 배지 (P0)
  • 커뮤니티(동아리·모임) 개설·가입·멤버십·역할(방장/멤버)과 커뮤니티 전용 그룹 채팅 자동 연동 (P0)
  • 게스트 초대: 정회원을 특정 방에 한시적으로 초대, 발화 권한 제한, 참여 기간 만료, 초대·수락·거절 흐름 (P0)
  • Room-외부엔티티 연결 메커니즘(contextType/contextId) 설계, 1번째 연동(커뮤니티) + 2번째 연동(goods 거래 채팅) 확정 (P0/P1)

Non-Goals

  • 비회원 토큰/링크 기반 게스트 접근 — 게스트는 가입된 userId를 가진 정회원으로 한정합니다. 비회원 초대는 이번 범위 밖입니다.
  • 커뮤니티 피드/게시판/이벤트 관리 — 커뮤니티는 멤버십·역할·전용 채팅방까지만 다루고, post 도메인과의 결합(모임 게시판)이나 이벤트(일정) 관리는 향후 별도 PRD로 다룹니다.
  • 음성/영상 통화 — 텍스트 메시지 실시간 전송만 다룹니다.
  • 미디어(이미지·파일) 첨부 — 텍스트 메시지만 지원합니다. 다음 단계에서 별도 검토합니다.
  • 메시지 전문 검색(full-text search) — 커서 기반 목록 조회만 유지합니다.
  • booking·ticketing 실제 채팅 연동 — 확장 메커니즘은 범용으로 설계하지만, 이번 PRD에서 FR로 구체화하는 도메인 연동은 커뮤니티(1번째)와 goods(2번째) 2건으로 한정합니다. booking·ticketing 연동은 향후 별도 PRD입니다.
  • 다중 디바이스 동시 접속 정교화 — 여러 기기에서 동시 로그인 시 읽음 상태는 단순 최종 갱신(last-write-wins)으로 처리하고, 기기 간 정교한 동기화는 다루지 않습니다.

User Scenarios

페르소나: 방장(Host, 커뮤니티 개설자), 멤버(Member, 커뮤니티 가입자), 게스트(Guest, 특정 방에 한시 초대된 정회원), 거래 당사자(Trader, goods 구매자/판매자)

  • (해피 패스) 방장이 “주말 축구 모임” 커뮤니티를 만들면 전용 그룹 채팅방이 자동 생성되고, 멤버를 초대하면 멤버가 채팅방에 자동 참여합니다.
  • (해피 패스) 멤버가 앱을 재실행하면 채팅방 목록에서 안읽은 메시지 수 배지와 최근 메시지 미리보기를 확인합니다.
  • (해피 패스) 방장이 커뮤니티 비멤버인 지인을 특정 채팅방에 게스트로 초대하고, 게스트가 초대를 수락해 대화에 참여하고 발화합니다.
  • (예외) 게스트가 초대를 거절하면 초대가 취소되고 방에 참여하지 않습니다.
  • (상태 보호) 게스트의 참여 기간이 만료되면 자동으로 방에서 방출되고, 이후 메시지 열람·발화가 차단됩니다.
  • (해피 패스) 구매자가 중고 상품 상세 화면에서 “채팅하기”를 누르면 판매자와의 1:1 거래 채팅방이 자동 생성되거나 기존 방으로 이동합니다.
  • (예외) WebSocket 연결이 끊긴 상태에서 메시지를 보내면 클라이언트가 재연결을 시도하고, 재연결 성공 시 끊긴 구간의 메시지를 backfill로 채웁니다.
  • (상태 보호) 멤버가 커뮤니티를 탈퇴하거나 방장에게 강퇴되면 연결된 그룹 채팅방에서도 자동으로 퇴장됩니다.
  • (빈 상태) 참여 중인 채팅방이 없는 신규 가입자는 채팅 목록에서 빈 상태 화면을 봅니다.

Benchmarking

제품명카테고리참조 패턴URL
당근마켓중고거래/지역 커뮤니티 앱채팅을 별도 서비스로 분리하고 WebSocket 연결 사용자는 FCM 지연과 무관하게 실시간 이벤트를 즉시 수신하도록 이중화. 중고거래·직거래 등 “거래 목적 채팅” 구조를 하나로 통일해 여러 도메인이 재사용 — 이번 PRD의 contextType/contextId 확장 메커니즘과 동일한 문제의식당근마켓 채팅 시스템이 현대화 되어온 과정, 당근의 모든 서비스가 만나는 교차로, 채팅
네이버 밴드모임/그룹 SNS모임(밴드) 단위로 참석 멤버 전용 그룹 채팅방을 만들어 모임 전후 소통을 지원 — 커뮤니티(밴드)와 그룹 채팅방이 1:1로 종속되는 이번 PRD의 FR-4와 동일한 패턴밴드 앱 - App Store
Slack팀 커뮤니케이션Single-Channel Guest(채널 1개만 접근)와 Multi-Channel Guest를 구분하고, 게스트의 메시지 발송·채널 생성·파일 공유를 관리자가 제한하며, 일정 기간 후 자동 비활성화(만료)하는 게스트 모델 — 이번 PRD의 FR-13(권한 제한)·FR-14(만료)의 직접 참조 대상Understand guest roles in Slack

Functional Requirements

커뮤니티 도메인

ID우선순위요구사항
FR-1P0이름·설명·공개 여부(공개/비공개)·스포츠 종목 카테고리를 지정해 커뮤니티를 개설할 수 있다
FR-2P0공개 커뮤니티는 즉시 가입, 비공개 커뮤니티는 방장 승인 후 가입된다
FR-3P0커뮤니티 멤버는 방장(Host, 1명) 또는 멤버(Member, N명) 역할을 가지며, 방장은 멤버를 강퇴하거나 방장 권한을 위임할 수 있다
FR-4P0커뮤니티 개설 시 전용 그룹 채팅방이 자동 생성되고, 커뮤니티 멤버는 가입과 동시에 해당 채팅방 참여자로 자동 등록된다
FR-5P0커뮤니티 탈퇴 또는 강퇴 시 연결된 그룹 채팅방에서도 자동으로 퇴장 처리된다

실시간 전송

ID우선순위요구사항
FR-6P0WebSocket/STOMP 연결을 통해 메시지를 실시간으로 송수신한다
FR-7P0참여자별 마지막으로 읽은 메시지 시점을 추적해 상대방에게 읽음 여부를 표시한다
FR-8P0상대가 입력 중임을 실시간으로 표시하고, 입력이 멈추면 수 초 내 자동으로 표시가 사라진다
FR-9P0채팅방 목록과 전체 배지에 참여자별 안읽은 메시지 수를 노출한다
FR-10P1WebSocket 연결이 끊기면 REST로 폴백하고, 재연결 성공 시 끊긴 구간의 메시지를 backfill로 채운다

게스트 초대

ID우선순위요구사항
FR-11P0방장은 특정 채팅방에 커뮤니티 비멤버인 정회원(userId 보유)을 게스트로 초대할 수 있다
FR-12P0게스트 초대는 수락/거절 흐름을 가지며, 수락 시 참여자로 추가되고 거절 시 초대가 취소된다
FR-13P0게스트는 발화 가능 여부를 초대 시 설정할 수 있고(읽기 전용 또는 발화 가능), 초대된 방 외의 해당 커뮤니티 컨텍스트 기능(멤버 목록 조회 등 커뮤니티 멤버십 범위의 기능)에는 접근할 수 없다
FR-14P0게스트 초대 시 참여 만료 시각(예: 7일)을 설정하며, 만료 도달 시 자동으로 방에서 방출되고 그 시점까지의 읽은 이력은 유지된다
FR-15P1방장은 만료 전에도 게스트를 수동으로 방출할 수 있다

FR-13 용어 정의: “게스트”는 전역 계정 롤이 아닙니다. domain/common/UserRoleName.kt에는 USER/ADMIN/FACILITY_OWNER/EVENT_HOST/GOODS_SELLER/OPERATIONS_MANAGER만 존재하고 GUEST 롤이 없습니다. 게스트는 정회원(UserRoleName=USER 등 기존 롤 유지)이 특정 방(또는 그 방이 속한 커뮤니티)에 대해 갖는 참여 스코프 속성이며, 그 컨텍스트 밖에서는 일반 정회원과 동일한 권한을 갖습니다.

확장 메커니즘

ID우선순위요구사항
FR-16P0Room이 contextType/contextId로 외부 도메인 엔티티와 연결될 수 있는 일반 확장 메커니즘을 제공한다
FR-17P0커뮤니티 그룹 채팅을 위 메커니즘의 1번째 연동 사례로 구현한다(contextType=COMMUNITY)
FR-18P1중고 상품 상세 화면의 “채팅하기”에서 구매자와 판매자(Product.ownerId) 간 1:1 거래 채팅방을 2번째 연동 사례로 생성/조회한다(contextType=GOODS_PRODUCT)

FR-18 도메인 선정 근거: Product.ktownerId(개인 판매자)를 명시적으로 보유해 구매자-판매자 1:1 관계가 명확합니다(domain/goods/entity/Product.kt#requireOwnedBy). 반면 Booking.ktuserId(예약자)와 slotId(시설 슬롯)만 가지며 대화 상대가 될 개인(시설 운영자 개인 계정)이 도메인 모델상 명확하지 않고, ticketing도 이벤트 주최자 대비 구매자 구도가 즉시 채팅으로 이어지지 않습니다. 따라서 goods가 확장 메커니즘의 2번째 연동으로 가장 자연스럽습니다.

기존 데이터 호환 설계 결정: contextType/contextId는 nullable로 추가합니다. 기존 DIRECT/GROUP 방(개인 1:1·순수 그룹 채팅)은 특정 외부 컨텍스트에 속하지 않으므로 두 컬럼을 null로 유지하고, 커뮤니티·goods 연동 방만 값을 채웁니다. Room/RoomParticipant에 컬럼을 추가하는 것은 additive 변경이라 기존 스키마·데이터와 충돌하지 않습니다.

Non-Functional Requirements

항목목표 수치비고
동시 WebSocket 세션로컬 docker 단일 인스턴스 기준 300세션 처리개인 프로젝트 규모 가정 초안 — Open Questions 참조
기존 MCP SSE 연결과 스레드풀 공유 영향신규 WebSocket 연결이 MCP SSE(application.yml:27-28)와 동일 Tomcat 인스턴스·스레드풀(max: 400, application.yml:4-11)을 공유한다는 전제로 설계·용량 산정WebSocket 구현 방식(동기 STOMP vs 비동기)에 따라 연결당 스레드 점유 여부가 달라지므로 TDD 단계에서 SSE 최대 연결 수 + WebSocket 목표 세션 수(300) 합산 기준 스레드풀 재산정 필요
메시지 전달 지연발신~수신 P95 500ms 이내동일 로컬 환경 기준
읽음 확인 반영 지연P95 1초 이내
재연결 정책지수 백오프로 재시도, 3회 연속 실패 시 REST 폴링으로 전환FR-10
인증WebSocket 연결 시에도 REST와 동일한 JWT 인증 필수미인증 연결 거부
게스트 접근 통제참여자 검증(기존 NotRoomParticipantException 계열) + 만료 시각 검사를 매 요청마다 수행FR-14
메시지 보존기존 소프트 삭제 정책 유지, 별도 만료 삭제 없음(무기한 보존)스토리지 증가 대응 필요 여부는 Open Questions

Operations

  • 지표: WebSocket 활성 연결 수, 초당 발송 메시지 수, 메시지 발신~수신 지연 P95/P99, 읽음 확인 반영 지연, 게스트 만료 배치 성공률, 커뮤니티별 활성 채팅방 수
  • 알림: WebSocket 연결 실패율이 5%를 초과하면 알림, 게스트 만료 배치가 실패하면 알림(기존 infrastructure/notification/gateway 채널 재사용)
  • 대시보드/로그: 커뮤니티별 활성 채팅방 수와 게스트 초대 수락률을 주기적으로 집계해 노출

Success Metrics

지표목표측정 방법
커뮤니티 그룹 채팅 D7 활성화율커뮤니티 가입 후 7일 내 채팅방 메시지 발송 비율 40% 이상가입 이벤트와 메시지 발송 이벤트 조인 집계
메시지 전달 지연 목표 달성률발신~수신 P95 500ms 이내 요청 비율 99% 이상클라이언트-서버 타임스탬프 로그 집계
게스트 초대 수락률60% 이상초대 발송 대비 수락 건수
goods 거래 채팅 전환율1단계는 목표치 없이 측정 전용으로 운영 — 2단계(FR-18 배포) 후 4주간 베이스라인 수집해 목표치 확정채팅방 생성(contextType=GOODS_PRODUCT) 대비 GoodsOrder 완료 건수

Milestones

단계범위
1단계커뮤니티 도메인(FR-15) + 실시간 전송 핵심(FR-69) + 게스트 초대 핵심(FR-1114) + 확장 메커니즘과 커뮤니티 연동(FR-1617)
2단계WebSocket 재연결 backfill(FR-10), 게스트 수동 방출(FR-15), goods 거래 채팅 연동(FR-18)
3단계 (향후, 범위 밖)booking·ticketing 연동, 미디어 첨부, 메시지 전문 검색

Open Questions

  • 게스트 초대 발신 권한을 방장으로만 한정할지, 일반 멤버도 초대할 수 있게 할지 — 초안은 방장만 가능으로 가정(Slack 게스트 초대 기본 정책 참조)
  • 동시 접속 300세션·P95 500ms는 실측 이전 추정치입니다. 실제 운영 데이터 확보 후 재조정이 필요합니다.
  • 메시지를 무기한 보존하는 정책이 스토리지 증가에 따라 유지 가능한지 별도 검토가 필요합니다.
  • 게스트가 여러 기기에서 동시 로그인할 경우의 읽음 상태 동기화는 이번 범위에서 제외했으나, 실사용 시 문제가 되면 재검토가 필요합니다.

Document History

날짜변경 내용
2026-07-04최초 작성 — Q1~Q4 확정 답변 반영(실시간 전송 P0 포함, 정회원 게스트 모델, 커뮤니티 도메인 신설, contextType 확장 메커니즘 + 커뮤니티·goods 2건 연동)
2026-07-04리뷰 반영(NEEDS_REVISION) — ① AS-IS를 “MCP 모듈이 이미 SSE 사용 중이며 Tomcat 스레드풀을 장기연결 대응으로 튜닝해둔 상태” 사실로 정정(application.yml:4-11,27-28 근거), ② NFR에 “기존 MCP SSE 연결과 스레드풀 공유 영향” 행 추가, ③ FR-13을 전역 롤이 아닌 커뮤니티/방 스코프 참여 속성 기준으로 재기술하고 UserRoleName.kt에 GUEST 롤이 없음을 근거로 용어 정의 각주 추가, ④ Success Metrics의 goods 거래 채팅 전환율을 “1단계 측정 전용, 2단계 이후 베이스라인 확정”으로 명시, ⑤ contextType/contextId nullable 유지(기존 방 호환) 설계 결정 각주 추가