[table-now] 예약자-사장님 1:1 실시간 채팅 기능 설계 및 구현

2025. 6. 4. 23:52·Dev Projects

🔍 구현 목적

`table-now`는 실시간 식당 예약 서비스입니다.

예약자와 가게 사장님 간의 실시간 소통을 지원하기 위해 1:1 채팅 기능을 구현했습니다.
단순한 메시지 전송이 아닌, 예약 건 단위로 안전하게 주고받을 수 있도록 설계하여, 예약 관련 문의 및 요청사항을 실시간으로 교환할 수 있습니다.

 


 

🧩 설계

1. ERD 설계 - 채팅방 없이 메시지만 저장

이 시스템은 예약(reservation) 단위로 채팅이 이뤄지므로, 별도의 채팅방 테이블은 필요하지 않습니다.

각 `reservation_id`는 고유한 채팅방 역할을 하며 예약이 존재해야만 채팅이 가능하므로, 채팅방 개념은 reservation에 종속됩니다.
모든 채팅 메시지는 chat_message 테이블에 저장됩니다.

💡 채팅방 테이블을 만들지 않은 이유
예약이 존재하지 않으면 채팅도 불가능하며, 채팅방 생성과 삭제 시점을 따로 관리할 필요가 없기 때문.

 

2. chat_message 테이블 구조


최종 테이블 구조

컬럼명 설명
id 채팅 메시지 ID (PK)
sender_id 메시지 전송자 (user ID, FK, 연관관계)
reservation_id 채팅이 연결된 예약 ID (연관관계 X)
owner_id 가게 사장님 user ID (역정규화)
reservation_user_id 예약자 user ID (역정규화)
content 메시지 내용
image_url 이미지 첨부 URL
is_read 읽음 여부
created_at 생성 일시

 

✅ 역정규화 결정 이유

현재 프로젝트의 핵심 도메인은 예약(reservation)입니다.

채팅 기능은 예약을 기준으로 생성 되지만, 예약의 비즈니스와 채팅 메시지는 직접적인 관련이 없습니다.

 

매 메시지 조회 시 JOIN이 발생하면 성능 저하와 코드 복잡도가 증가하므로, 정규화를 해제하고 단순 ID만 저장하는 구조로 선택했습니다.

⇒ 역할 판별의 단순화와 성능 향상을 위해 `owner_id`와 `reservation_user_id`를 `chat_message` 테이블에 함께 저장하는 역정규화를 적용했습니다.

sender_id가 reservation_user_id와 같으면 → 예약자 메시지
sender_id가 owner_id와 같으면 → 사장님 메시지

 

🖇️ 연관 관계

`owner_id`와 `reservation_user_id` 필드를 따로 구성하면서 `reservation_id`를 외래키로 참조할 필요가 없어졌습니다.

이미 역정규화한 필드들로 역할 판단이 가능하므로, `reservation` 객체 전체를 불러올 이유가 없기 때문에,

굳이 연관관계를 맺지 않고, ID만 저장하도록 수정했습니다. (단순 Long으로 처리)

 

반면, `sender_id` 필드는 연관관계 유지했습니다.
엔티티에서 `User sender` 로 사용자 정보 직접 탐색하기 위해 `@ManyToOne` 연관관계 유지하는 판단을 내렸습니다.
(메시지 DTO 구성 시 `sender.getName()` 호출할 예정)

 


 

📝 API 설계

1. 채팅 가능 여부 확인

목적: WebSocket 연결 전 사전 검증
조건: 예약 상태가 `RESERVED`일 때만 채팅 가능
API: `GET /api/v1/chats/{reservationId}/availability`

응답 예시:
{
  "reservationId": 123,
  "userId": 45,
  "reason": "RESERVED",
  "available": true
}
 CANCELED, COMPLETED 상태는 채팅 불가 (403 반환)

 

2. 채팅 메시지 읽음 처리

목적: 본인이 받지 않은 메시지를 읽음 처리
API: `PATCH /api/v1/chats/{reservationId}/read`

응답 예시:
{
  "reservationId": 123,
  "userId": 45,
  "message": "5개의 메시지 읽음 처리가 완료되었습니다."
}
채팅방 내 본인이 아닌 상대방이 보낸 메시지만 읽음 처리 대상

 

3. 채팅 메시지 조회

목적: 페이지네이션 기반 채팅 메시지 조회
API: `GET /api/v1/chats/{reservationId}`

응답 예시:
{
  "content": [
    {
      "id": 1,
      "senderId": 45,
      "senderName": "홍길동",
      "content": "안녕하세요!",
      "imageUrl": null,
      "createdAt": "2025-06-01 14:30:00",
      "isRead": false
    }
  ],
  "page": 0,
  "size": 20,
  "totalElements": 35,
  "totalPages": 2,
  "last": false
}
Pageable을 활용해 페이지네이션 지원, 기본 page=0, size=20

 


 

💬 WebSocket 기반 채팅 기능 구현

1. WebSocket 관련 경로

- `CONNECT /ws/chat` WebSocket 연결
- `SUBSCRIBE /topic/chat/{reservationId}` 예약 ID 채팅방 구독
- `SEND /app/chat/message` 메시지 전송 요청

 

2. 채팅 테스트

 `예약 ID: 1 / 가게 ID: 1 / 유저 ID: 1`인 예약이 존재할 때

예약자와 사장님 각각이 WebSocket에 연결하고, 메시지를 주고받는 시나리오 테스트입니다.

 

- 일반 유저 WebSocket 연결

WebSocket 연결/구독 로그
테스트 화면
WebSocket 전송/수신 로그

 

- 가게 사장 WebSocket 연결

WebSocket 연결/구독 로그
테스트 화면
WebSocket 전송/수신 로그

 

- 메시지 DB 저장

 

- 예약과 관련 없는 유저가 채팅 참여

 

3. 구현 코드

https://github.com/spring-team-7/table-now/pull/165

 

[feat] 1:1 채팅 메시지 저장 및 WebSocket 테스트 페이지 구현 by mannaKim · Pull Request #165 · spring-team-7/ta

🔗 Issue Number close #158 📝 작업 내역 spring-boot-starter-websocket 의존성 추가 WebSocketConfig 설정 및 JwtHandshakeInterceptor 적용 테스트용 WebSocket 클라이언트 페이지 생성 예약 ID, AccessToken 입력 후 WebSocket 연

github.com

 


 

💻 API 구현

WebSocket용 컨트롤러 `ChatMessageController`와 분리된 REST API용 컨트롤러 `ChatController` 생성하여 구현했습니다.

 

  • ChatMessageController
    • `@Controller` + `@MessageMapping`
  • ChatController
    • `@RestController` + `@GetMapping` 등
구분 용도 Annotation 사용 예시
`ChatMessageController` WebSocket 수신 처리 `@Controller`
`@MessageMapping`
메시지 수신 후 브로드캐스트
`ChatController` REST API 처리 `@RestController`
`@RequestMapping`
채팅 가능 여부 확인,
메시지 조회 등

 

 

https://github.com/spring-team-7/table-now/pull/180

 

[feat] 채팅 가능 여부 확인 및 메시지 목록/읽음 처리 API 구현 by mannaKim · Pull Request #180 · spring-team

🔗 Issue Number close #168 📝 작업 내역 GET /api/v1/chats/{reservationId}: 메시지 목록 조회 @PageableDefault로 page=0, size=20 자동 매핑 PATCH /api/v1/chats/{reservationId}/read: 사용자 기준 읽음 처리 채팅방(reservatio...

github.com

 


 

✅ 결론

  • reservation_id를 기준으로 채팅방을 별도로 관리하지 않고, 메시지 중심의 구조로 단순화
  • 역할 판별을 위한 owner_id, reservation_user_id 필드 도입으로 JOIN 최소화
  • RESTful API를 통해 채팅 가능 여부 확인 및 메시지 CRUD 기능 제공

 

저작자표시 비영리 변경금지 (새창열림)

'Dev Projects' 카테고리의 다른 글

[table-now] Spring Batch 개념과 정산 자동화 구조 정리  (0) 2025.06.18
[table-now] 1:1(예약자:가게) 채팅 기능 고도화를 위해 RabbitMQ Relay를 적용한 이유  (0) 2025.06.05
[table-now] Spring Security의 oauth2Login() 대신 WebClient를 선택한 이유  (3) 2025.05.28
[table-now] 의사결정 기록 - 외부 연동 시스템 예외 처리 강화 리팩토링  (0) 2025.05.23
[table-now] S3 이미지 업로드 - Presigned Url 방식으로 구현하기  (1) 2025.05.23
'Dev Projects' 카테고리의 다른 글
  • [table-now] Spring Batch 개념과 정산 자동화 구조 정리
  • [table-now] 1:1(예약자:가게) 채팅 기능 고도화를 위해 RabbitMQ Relay를 적용한 이유
  • [table-now] Spring Security의 oauth2Login() 대신 WebClient를 선택한 이유
  • [table-now] 의사결정 기록 - 외부 연동 시스템 예외 처리 강화 리팩토링
기만나🐸
기만나🐸
공부한 내용을 기록합시다 🔥🔥🔥
  • 기만나🐸
    기만나의 공부 기록 🤓
    기만나🐸
  • 전체
    오늘
    어제
    • ALL (147)
      • TIL (Today I Learned) (56)
      • Dev Projects (15)
      • Algorithm Solving (67)
        • Java (52)
        • SQL (15)
      • Certifications (8)
        • 정보처리기사 실기 (8)
  • 인기 글

  • 태그

    완전탐색
    java
    join
    DFS
    자료구조
    시뮬레이션
    Subquery
    greedy
    dp
    websocket
    BOJ
    mysql
    BFS
    GROUP BY
    sql
    CSS
    programmers
    bootstrap
    jpa
    프로그래머스
    jwt
    다이나믹프로그래밍
    HTML
    jQuery
    그리디
    Google Fonts
    백준
    javascript
    백트래킹
    Firebase
  • 최근 글

  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
기만나🐸
[table-now] 예약자-사장님 1:1 실시간 채팅 기능 설계 및 구현
상단으로

티스토리툴바