1:1 실시간 채팅 기능 ‐ 기술적 의사결정 - fitpassTeam/fitpass GitHub Wiki

1:1 실시간 채팅 기능 - 기술적 의사결정

문제 정의: 실시간 통신 방식 선택 및 아키텍처 설계

배경

FitPass 플랫폼에서는 **사용자(User)**와 헬스장 운영자(Gym Owner) 간의 1:1 실시간 채팅 기능이 필수적이었습니다.

기존 REST API 방식으로는 실시간성 요구를 만족할 수 없었기에 WebSocket 기반의 양방향 통신이 필요했습니다.

핵심 요구사항

  • 실시간 메시징
  • 1:1 사용자-체육관 간 독립 채팅방
  • 채팅 내역 저장 (영속성)
  • 다중 사용자 접속 확장성

기술 스택 비교 및 선택

1. 통신 방식 비교

방식 실시간성 리소스 효율성 구현 복잡도 양방향
HTTP Polling ❌ 낮음 ❌ 낮음 ✅ 쉬움 ✅ 가능
Server-Sent Events ⚠️ 중간 ⚠️ 중간 ⚠️ 보통 ❌ 단방향
WebSocket ✅ 높음 ✅ 효율적 ⚠️ 중간 ✅ 양방향

선택: WebSocket + STOMP 프로토콜


2. 메시지 브로커 선택

선택: Spring 내장 브로커(SimpleBroker)

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
    registry.setApplicationDestinationPrefixes("/app");
    registry.enableSimpleBroker("/topic", "/queue");
    registry.setUserDestinationPrefix("/user");
}

}

선택 이유:

내장 브로커로 운영 복잡도 ↓

외부 의존성 없음 (Redis, RabbitMQ 불필요)

초기 서비스에는 충분한 성능

1. 메시지 라우팅 전략

선택: 하이브리드 (공용 + 사용자 개별 전송)

@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public") // 전체 사용자에게 브로드캐스트
public ChatMessageResponseDto sendMessage(@Payload ChatMessageRequestDto request) {
    // DB 저장 후 개별 사용자에게 전송
    String receiverDestination = "/user/" + receiverId + "/queue/messages";
    messagingTemplate.convertAndSend(receiverDestination, responseDto);
    return responseDto;
}

장점:

전체 채널 + 개별 사용자 동시 지원

보안 분리 (user prefix 기반)

확장 시 그룹 채팅 등 대응 가능

DB 설계 요약

  1. 채팅방 (ChatRoom)
@Entity
public class ChatRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

// 유저 (1:1 채팅 대상)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

// 체육관 (운영자 또는 트레이너 소속)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "gym_id", nullable = false)
private Gym gym;

}

  1. 메시지 (ChatMessage)
@Entity
public class ChatMessage {
@ManyToOne(fetch = FetchType.LAZY)
private ChatRoom chatRoom;

@Enumerated(EnumType.STRING)
private SenderType senderType; // USER / GYM

@Column(nullable = false)
private String content;

}

설계 특징

  • 1:1 관계 기반 채팅방 유지 (User - Gym 쌍 기준)
  • 발신자 타입 구분(SenderType)으로 프론트 렌더링 최적화
  • 지연 로딩(FetchType.LAZY) 기반 성능 최적화
  • 향후 확장 전략

1. 메시지 브로커 확장 (Redis)

@Configuration
@Profile("production")
public class ScalableWebSocketConfig {
    // Redis 기반 멀티 노드 메시지 브로커 연동
}

2. 기능 확장 계획

  • 파일 전송 기능

    이미지, 문서 등 다양한 파일을 채팅 내에서 주고받을 수 있도록 기능 추가

  • 읽음 확인(Read Receipt)

    사용자별 메시지 읽음 상태 표시 기능 구현

  • 푸시 알림 연동

    • FCM (Firebase Cloud Messaging): 모바일 앱 푸시 알림
    • Web Push: 브라우저 푸시 알림 지원

결론 및 회고

핵심 성공 요인

  • 단순한 구조
    • 내장 브로커(SimpleBroker) 기반으로 빠른 MVP 개발 및 운영 가능
    • 복잡성 최소화로 유지보수 용이
  • 표준 프로토콜(STOMP) 채택
    • 다양한 클라이언트(Web, Mobile)와 호환성 보장
    • Redis, RabbitMQ 등 메시지 브로커로 무중단 확장 가능
  • 점진적 확장 전략
    • 초기에는 단일 서버, 내장 브로커 사용
    • 추후 Redis 기반 멀티 노드 환경으로 자연스러운 확장 가능

https://sang914.tistory.com/18

⚠️ **GitHub.com Fallback** ⚠️