mini Projects/ToyTalk 키덜트 대화방 서비스
(25.05.27) ToyTalk 기획 및 WebSocket 활용 방안 / 대화방 CRUD 구현
개요
기획배경 | • 키덜트 정보를 플랫폼 구애없이 대화방에서 나눌수 있는 특화된 채팅 플랫폼의 니즈 • WebSocket 프레임 구현 서비스 구축 |
기획목적 | 키덜트 사용자들이 공통 주제로 자유롭게 대화할 수 있는 오픈채팅방 플랫폼 제공 |
주요 기능요약 | • 가입자는 누구든지 특정 키덜트 주제(장난감 : 아트토이, 피규어, 프라모델 등) 주제로 대화방을 생성 가능 • 서비스 가입자와 비밀번호 설정된 대화방일 경우, 비밀번호를 입력한 자만 대화방에 입장 가능 • WebSocket을 활용해 인메모리(Spring) 메시지 브로커를 활용한 실시간 대화 가능 |
User Flow / User Scenario
1. 채팅방 생성 요청
- 요청 엔드포인트: POST /chatrooms
- 요청 본문에는 title, category, isPrivate, password(옵션) 등 포함
- 인증된 사용자만 요청 가능
2. 방 생성 후 방장 자동 입장
- 방 생성 시 방장이 자동으로 **chatroom_members**에 등록되도록 처리
- 방 생성 후 방장 유저를 해당 방에 자동 등록 (is_joined = true, joined_at = now)
- FE에서 소켓 연결을 직접 수행하게 유도 (stompClient.connect() → /pub/chatroom.{roomId})
- 방 생성 응답 시 roomId, 입장용 destination 등을 같이 반환해주면 FE가 바로 연결
3. 다른 사용자들은 로그인 후 전체 채팅방 조회
- GET /chatrooms
- 비공개 방은 목록에는 뜨되, 입장 시 인증 필요
- 비밀번호 보호된 방은 비밀번호를 입력 UI로 처리 but 요청시엔 한번에 4번 참조
4. 특정 채팅방 연결 요청
- POST /chatrooms/{roomId}/join 형태 추천
- body에 password 포함
- 서버에서 검증
- 해당 채팅방이 존재여부
- 비공개 방일 경우 비밀번호 일치 여부
- 최초 검증 성공 시 chatroom_members에 참여 기록 저장 (is_joined = true)
5. 사용자 채팅방 입장 (WebSocket 연결)
- STOMP: /pub/chatroom.{roomId} 로 SEND, /topic/chatroom.{roomId} 로 SUBSCRIBE
- WebSocket 연결 전에 4번 검증이 끝나 있어야 함
- 메시지 송수신은 이후부터 가능
6. 사용자가 채팅방 나가기
- POST /chatrooms/{roomId}/leave
- 서버는 **chatroom_members**에서:
- is_joined = false
- left_at = 나간시간
- FE는 WebSocket 연결 해제 처리
7. 방장이 채팅방 삭제 요청
- DELETE /chatrooms/{roomId}
- 삭제 권한은 created_by == 로그인한 사용자 인지 확인
- 서버는 트랜잭션으로 처리:
- 해당 방의 status = DEACTIVATED
- chatroom_members 전원 is_joined = false, left_at = now
- 활성 연결 WebSocket 세션 강제 끊기 (가능하면 메시지로 알림 추후)
요구사항
Key Feature
- 회원가입 / 로그인 (Boiler Plate 활용)
- JWT를 통한 Spring Security의 인가 인증 필터 적용
- Redis 활용 Refresh Token 관리
- 대화방 (chatroom)
- 가입된 사용자는 방장으로 비밀번호를 사용 또는 미사용해서 채팅방을 제목과 함께 생성
- 방장만 자신이 만든 채팅방 삭제
- 사용자는 전체 채팅방 목록에서 원하는 채팅방을 선택해 입장(가입)해 채팅 가능
ERD
- 실시간 대화방을 기획 기준으로 대화의 내용을 초기 단계에서 저장하지 않음
- FE 에서 각 클라이언트 로컬에서 캐싱으로 저장하는 형태
개발 방향
https://www.youtube.com/watch?v=9UUi5s_hkBU&t=133s
- HTTP 1.1 기반의 WebSocket Frame과 STOMP 를 통해 Pub/Sub 방식으로 구현
- 위의 우아한테크(우아한 형제 강의)를 기본으로 WebSocket을 통한 Chat구성
- 단, 채팅방 개설, 비밀번호가 있는 방 등 Feature역시 동시 구현
채팅방 CRUD 구현
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/chatrooms")
public class ChatroomController {
private final ChatroomService chatroomService;
@GetMapping
public ResponseEntity<List<ChatroomResponseDTO>> getAllChatrooms() {
List<ChatroomResponseDTO> chatrooms = chatroomService.getAllChatrooms();
return ResponseEntity.status(HttpStatus.OK).body(chatrooms);
}
@PostMapping
public ResponseEntity<ChatroomResponseDTO> createChatroom(@Valid @RequestBody ChatRoomRequestDTO request,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
ChatroomResponseDTO createdChatroom = chatroomService.createChatroom(request, userDetails.getUser());
return ResponseEntity.status(HttpStatus.CREATED).body(createdChatroom);
}
@DeleteMapping("/{chatroomId}")
public ResponseEntity<Void> deleteChatroom(@PathVariable String chatroomId,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
chatroomService.deleteChatroom(UUID.fromString(chatroomId), userDetails.getUser());
return ResponseEntity.noContent().build();
}
}
- WebSocket을 통해서 직접적으로 특정 대화방에 입장(Sub)을 하기 위해서는 이미 그 대화방에 대한 정보 = Chatroom 객체와 DB안에 데이터가 존재해야함
- createdBy 인 방장이 POST 요청과 함께 Chatroom 을 생성 / 삭제가 가능 하도록
- 그 외에, 채팅방에 입장할때의 사용자 검증, 그리고 들어간 시간, 나간 시간 기록은 WebSocket 메시지의 요청시 진행할 수 있도록 분리되어 다른 WebSocket Controller에서 구현 계획
public ChatroomResponseDTO createChatroom(ChatRoomRequestDTO request, User user) {
String title = request.getTitle();
String category = request.getCategory();
UUID userId = user.getId();
Boolean isPrivate = request.getIsPrivate();
String password = null;
if(isPrivate) {
String inputPassword = request.getPassword();
if(inputPassword == null || inputPassword.isEmpty()) {
throw new IllegalArgumentException("비공개 대화방은 비밀번호를 입력해야합니다.");
} else {
password = passwordEncoder.encode(inputPassword);
}
}
Chatroom chatroom = Chatroom.builder()
.title(title)
.category(category)
.status(ChatroomStatus.ACTIVATED)
.isPrivate(isPrivate)
.password(password)
.createdBy(userId)
.build();
return new ChatroomResponseDTO(chatroom);
}
@Transactional
public void deleteChatroom(UUID chatroomId, User user) {
Chatroom chatroom = chatroomRepository.findActivatedRoomById(chatroomId).orElseThrow(
()-> new IllegalArgumentException("해당 대화방은 없습니다.")
);
if(!chatroom.getCreatedBy().equals(user.getId())) {
throw new IllegalArgumentException("방장만 대화방을 삭제할 수 있습니다.");
}
chatroom.updateStatus(ChatroomStatus.DEACTIVATED);
List<ChatroomMember> members = chatroomMemberRepository.findAllByChatroomId(chatroomId);
LocalDateTime now = LocalDateTime.now();
for (ChatroomMember member : members) {
member.updateIsJoined(false);
member.updateLeftAt(now);
}
}
- 대화방을 생성할 때는 요청 된 정보중 비공개 대화방을 생성하고자 할 때, password를 적용할 수 있도록 구현
- 즉, 비공게 대화방 체크 표시를 하지 않거나, 다른 FE에서 구현이 없을 경우엔 password를 입력해도 무조건 null로 객체가 만들어지게끔 로직
- Delete 는 Soft Delete로 updateState를 통해서 데이터 자체를 삭제하기 보다 DEACTIVATED 상태로 변경하여 저장 할 수 있도록 함
- 삭제 시, 동시에 findAllByChatroomId 메서드에 따로 @Query 를 사용해 모든 Roomid를 가지고 있는 모든 Chatroom Member 들을 전부 나가게끔 처리
- updateIsJoined, updateLeftAt 사용
'mini Projects > ToyTalk 키덜트 대화방 서비스' 카테고리의 다른 글
(25.06.09) 전체 대화방 조회용 Pagenation 구현 (0) | 2025.06.09 |
---|---|
(25.06.02) Thymleaf를 통한 회원가입/로그인 FE 구현 (1) | 2025.06.03 |
(25.05.30) ToyTalk WebSocket을 활용한 Chat 기능 구현 (Spring Boot) (0) | 2025.05.30 |