본문 바로가기
mini Projects/ToyTalk 키덜트 대화방 서비스 2025. 5. 28.

(25.05.27) ToyTalk 기획 및 WebSocket 활용 방안 / 대화방 CRUD 구현

개요

기획배경 • 키덜트 정보를 플랫폼 구애없이 대화방에서 나눌수 있는 특화된 채팅 플랫폼의 니즈
• WebSocket 프레임 구현 서비스 구축
기획목적 키덜트 사용자들이 공통 주제로 자유롭게 대화할 수 있는 오픈채팅방 플랫폼 제공
주요 기능요약 • 가입자는 누구든지 특정 키덜트 주제(장난감 : 아트토이, 피규어, 프라모델 등) 주제로 대화방을 생성 가능
• 서비스 가입자와 비밀번호 설정된 대화방일 경우, 비밀번호를 입력한 자만 대화방에 입장 가능
• WebSocket을 활용해 인메모리(Spring) 메시지 브로커를 활용한 실시간 대화 가능

User Flow / User Scenario

1. 채팅방 생성 요청

  • 요청 엔드포인트POST /chatrooms
  • 요청 본문에는 titlecategoryisPrivatepassword(옵션) 등 포함
  • 인증된 사용자만 요청 가능

2. 방 생성 후 방장 자동 입장

  • 방 생성 시 방장이 자동으로 **chatroom_members**에 등록되도록 처리
  • 방 생성 후 방장 유저를 해당 방에 자동 등록 (is_joined = truejoined_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 = falseleft_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  사용