Redis Pub/Sub을 활용한 채팅 구현의 여정 - Chat Domain(Entity ~ Controller)

2024. 12. 8. 02:34·Redis/Redis 채팅
목차
  1. [이전글]
  2. [소개]
  3. [Entity - Chatting Room]
  4. [Controller - ChatController]
728x90
반응형
SMALL

[이전글]

이전 글에는 Redis와 채팅을 사용하기 위한 Config파일들을 알아보았다.

https://sunro1994.tistory.com/254

 

Redis Pub/Sub을 활용한 채팅 구현의 여정 - 환경설정

https://sunro1994.tistory.com/253 Redis Pub/Sub을 활용한 채팅 구현의 여정 - 개념편수많은 우여곡절 끝에 배포된 서버에 채팅 기능을 적용하게 되었다!간단히 말하자면 원래 담당했던 백엔드 팀원이 던져

sunro1994.tistory.com

 

[소개]

이전 글에서는 Config파일을 사용해서 어떻게 Redis의 Pub/Sub 그리고 Websocket설정을 하는지 알아보았다.

이번에는 Chat 도메인을 어떻게 설계했는지 Entity부터 Service까지 하나한 알아보려고 한다.

채팅 도메인의 구조다.

나는 채팅방을 DB에 저장하고 채팅내용은 Redis에 저장한다. Redis의 읽기 및 쓰기 전략에는 Redis에 저장하는 데이터들의 TTL과 DB와의 정합성이 중요하지만 나는 아직 DB에 채팅을 저장하는게 좋은 방법인지 고민되어 현재는 Redis에 3일의 TTL을 두었다.

3일도 어떻게보면 매우 짧은 기간이라는 생각이 들어 수정할 예정이다. 왜냐하면 미수신 메시지를 유저의 앱을 가동시키고 채팅을 접속할때 미수신 데이터를 보내줘야하는데 유저가 3일안에 들어오지 않으면 채팅 기록은 클라이언트 로컬에도 남지 않기 때문이다.

여튼! entity - controller - service순으로 코드를 공개하려고 한다. repository는 JPARepository인터페이스에 메서드명이 그대로 들어가기 때문에 코드를 추가하지는 않겠다.

 

 

[Entity - Chatting Room]

roomId로 방을 구분했으며, sender와 reciever로 메세지 전송을 하는 사람과 받는 사람을 구분했다.

senderImage와 recieverImage는 각 유저의 프로필 이미지이다. 카톡과 같이 닉네임과 Image를 함께 반환하기 위함이다.

lastTime은 해당 채팅방의 마지막 접속 시간을 나타낸다. 오랫동안 사용하지 않는 채팅방은 일정 시간이 지나면 기록을 삭제 시킨다.

@Entity
@Getter
@Setter
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public class ChatRoom {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long roomId;

    private Long sender;

    private Long receiver;

    private String title;

    private Timestamp lastTime;

    private String senderImage;

    private String receiverImage;

    public static ChatRoom to(Long receiver, Long sender, String findSenderProfileImage, String roomTitle, String findReceiverProfileImage) {
        return ChatRoom.builder()
                .receiver(receiver)
                .sender(sender)
                .senderImage(findSenderProfileImage)
                .title(roomTitle)
                .receiverImage(findReceiverProfileImage)
                .lastTime(Timestamp.valueOf(LocalDateTime.now()))
                .build();

    }
}

 

[Controller - ChatController]

이 부분은 각 컨트롤러를 뜯어서 설명하겠다.

@RestController
@RequestMapping("/api/v1/chat")
@RequiredArgsConstructor
@Slf4j
public class ChatController {

    private final ChatService chatService;
    private final S3FileUtil s3FileUtil;

    @MessageMapping("/chat")
    public void message(@Payload ChattingRequestDto message) throws IOException {

        chatService.sendMessageToPublisher(message);
    }

    @Operation(summary = "미수신 메시지 조회", description = "주어진 lastTime 이후의 메시지들을 조회합니다.")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "미수신 메시지 조회 성공"),
            @ApiResponse(responseCode = "400", description = "잘못된 요청"),
            @ApiResponse(responseCode = "500", description = "서버 오류")
    })
    @GetMapping("/get-unreceived-messages")
    public ResponseEntity<List<ChattingResponseDto>> getUnreceivedMessages(
            @RequestParam Long roomId,
            @RequestParam String lastTime // 마지막으로 받은 메시지 시간
    ) {
        // 마지막으로 받은 시간 이후의 메시지를 조회하는 서비스 호출
        Timestamp parsedLastTime = chatService.getParsedLastTime(lastTime);
        List<ChattingResponseDto> unreceivedMessages = chatService.getMessagesAfterLastTime(roomId, parsedLastTime);

        return ResponseEntity.ok(unreceivedMessages);
    }

    @PostMapping("/get-or-create")
    public ResponseEntity<ChatRoomCreateResponse> getOrCreateChatRoom(
            @AuthenticationPrincipal CustomUserDetail userDetail,
             @RequestParam  Long receiver,
            @RequestParam  String roomTitle
    ) {
        ChatRoomCreateResponse chatRoomCreateResponse=

                chatService.getOrCreateChatRoom(userDetail.getUuid(), receiver,roomTitle);

        return ResponseEntity.ok(chatRoomCreateResponse);
    }

    @GetMapping("/chatroom-list")
    public ResponseEntity<List<ChatRoomResponseDto>> getChatRoomList(
            @AuthenticationPrincipal CustomUserDetail userDetail
    ) {
        return ResponseEntity.ok(chatService.getChatRoomList(userDetail.getUuid()));
    }

    @PostMapping("/leave")
    public void leaveRoom(Long roomId) {
        chatService.leaveRoom(roomId);
    }

    @PostMapping(value = "/get-imageUrl", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<String> getImageUrl(
            @RequestPart("image") MultipartFile image) {
        String uploadedImage = s3FileUtil.upload(image);
        return ResponseEntity.ok(uploadedImage);
    }
}

 

1. 채팅 전송을 감지하는  API

@MessageMapping("/chat") 어노테이션은 "/chat"으로 들어오는 요청에 대해 감지하고 채팅 메시지를 가져올 수 있도록 한다.

ChattingRequestDto의 필드와 동일한 데이터를 페이로드에 담아서 클라이언트에서 보내면 이 메세지를 저장하는 로직을 Service레이어에서 수행한다.

@MessageMapping("/chat")
    public void message(@Payload ChattingRequestDto message) throws IOException {

        chatService.sendMessageToPublisher(message);
    }

 

 

2. 미수신 메세지를 받는 API

유저가 마지막으로 접속했던 시간을 체크해 그 이후의 채팅 기록을 가져오는 API이다.

위에서 말했듯이 해당 채팅 기록은 Redis 저장소에서 가져오기 때문에 Redis에 저장한 데이터의 TTL을 적절히 처리해줘야한다.

만약 Redis에 데이터가 없어 Cache Miss가 발생했다면 DB에서 데이터를 가져와야 하며 성능 저하가 발생할 수 도 있기 때문에 전략을 잘 짜야 할 것이다.

    @GetMapping("/get-unreceived-messages")
    public ResponseEntity<List<ChattingResponseDto>> getUnreceivedMessages(
            @RequestParam Long roomId,
            @RequestParam String lastTime // 마지막으로 받은 메시지 시간
    ) {
        // 마지막으로 받은 시간 이후의 메시지를 조회하는 서비스 호출
        Timestamp parsedLastTime = chatService.getParsedLastTime(lastTime);
        List<ChattingResponseDto> unreceivedMessages = chatService.getMessagesAfterLastTime(roomId, parsedLastTime);

        return ResponseEntity.ok(unreceivedMessages);
    }

 

3. 채팅방 생성 API

이전에 생성한 기록이 있다면 해당 채팅방 정보를 반환하고 그게 아니라면 새로운 채팅방을 생성한다.

생성한 기록이 있는지는 userDetail.getUuid와 receiver, roomTitle을 모두 대조하여 일치하는 방이 있는지 탐색한다.

    @PostMapping("/get-or-create")
    public ResponseEntity<ChatRoomCreateResponse> getOrCreateChatRoom(
            @AuthenticationPrincipal CustomUserDetail userDetail,
             @RequestParam  Long receiver,
            @RequestParam  String roomTitle
    ) {
        ChatRoomCreateResponse chatRoomCreateResponse=

                chatService.getOrCreateChatRoom(userDetail.getUuid(), receiver,roomTitle);

        return ResponseEntity.ok(chatRoomCreateResponse);
    }

 

4. 기타 API들

매우 간단한 API들이라 한 번에 설명하겠다

/chatroom-list는 채팅방 목록을 반환하는 API

/leave는 채팅방을 나가는 요청의 API

/get-imageUrl은 채팅에 이미지를 첨부할경우 전처리를 하는 API이다. 이전에는 byte로 사진데이터를 받아 처리하려고 했지만 이 부분에서 데이터가 1.5배 더 커지고 성능에 저하가 될 수도 있기 때문에 MultiPart타입의 데이터로 사진을 받아 채팅 데이터와 함께 String으로 S3에 저장된 이미지의 파일명을 넣는다.

@GetMapping("/chatroom-list")
    public ResponseEntity<List<ChatRoomResponseDto>> getChatRoomList(
            @AuthenticationPrincipal CustomUserDetail userDetail
    ) {
        return ResponseEntity.ok(chatService.getChatRoomList(userDetail.getUuid()));
    }

    @PostMapping("/leave")
    public void leaveRoom(Long roomId) {
        chatService.leaveRoom(roomId);
    }

    @PostMapping(value = "/get-imageUrl", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<String> getImageUrl(
            @RequestPart("image") MultipartFile image) {
        String uploadedImage = s3FileUtil.upload(image);
        return ResponseEntity.ok(uploadedImage);
    }

 

다음 글에서는 Service와 Service와 연관된 다른 도메인(Notification)을 알아보겠다. 함께 알아보는 이유는 유저가 미접속시 알림을 보내는 기능이 추가되어 있기 때문이다.

728x90
반응형
SMALL

'Redis > Redis 채팅' 카테고리의 다른 글

Redis Pub/Sub을 활용한 채팅 구현의 여정 - Service 레이어  (0) 2024.12.19
Redis Pub/Sub을 활용한 채팅 구현의 여정 - 환경설정  (0) 2024.11.29
Redis Pub/Sub을 활용한 채팅 구현의 여정 - 개념편  (0) 2024.11.28
[Redis] SpringBoot + Redis Pub/Sub 으로 채팅 구현 하기  (0) 2024.07.18
  1. [이전글]
  2. [소개]
  3. [Entity - Chatting Room]
  4. [Controller - ChatController]
'Redis/Redis 채팅' 카테고리의 다른 글
  • Redis Pub/Sub을 활용한 채팅 구현의 여정 - Service 레이어
  • Redis Pub/Sub을 활용한 채팅 구현의 여정 - 환경설정
  • Redis Pub/Sub을 활용한 채팅 구현의 여정 - 개념편
  • [Redis] SpringBoot + Redis Pub/Sub 으로 채팅 구현 하기
공부하고 기억하는 공간
공부하고 기억하는 공간
IT 비전공자로 시작하여 훌륭한 개발자가 되기 위해 공부하고 있는 공간입니다. 틀린 내용이나 부족한 부분이 있으면 댓글로 알려주세요 바로 수정하겠습니다.
IT - railroadIT 비전공자로 시작하여 훌륭한 개발자가 되기 위해 공부하고 있는 공간입니다. 틀린 내용이나 부족한 부분이 있으면 댓글로 알려주세요 바로 수정하겠습니다.
    250x250
  • 공부하고 기억하는 공간
    IT - railroad
    공부하고 기억하는 공간
  • 전체
    오늘
    어제
    • 분류 전체보기 (325)
      • 면접 준비 (22)
        • OS (6)
        • Spring Security (0)
        • Java (3)
        • DB (11)
        • Network (3)
      • ElasticSearch (2)
      • Kafka (4)
      • Spring (22)
        • Spring Cloud (7)
        • Security6 (5)
        • JPA (12)
        • 프로젝트 리팩토링 회고록 (4)
        • Logging (8)
        • Batch (2)
      • Redis (17)
        • Redis 개념 (8)
        • Redis 채팅 (5)
        • Redis 읽기쓰기 전략 (1)
      • AWS (11)
      • 리눅스 (29)
        • 리눅스 마스터 2급 (5)
        • 네트워크(기초) (7)
        • 리눅스의 이해 (6)
        • 리눅스의 설치 (2)
        • 리눅스 운영 및 관리 (6)
      • JAVA-기초 (16)
        • JAVA기본 (11)
        • Design Pattern (5)
      • JSP (27)
        • JSP 기본 개념 (10)
        • JSP (1)
      • SQL (1)
      • TIL (36)
      • 문제 풀이 (2)
        • Programmers (9)
        • 백준 문제풀이 (28)
      • JavaScript (10)
      • HTML (17)
      • Ngrinder (1)
        • Ngrinder 문서 정리 (1)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      JavaScript
      프로그래머스
      jsp request
      리눅스
      java
      redis 채팅
      자바
      JSP
      CSS
      redis
      리눅스마스터2급정리
      Spring
      자바 면접질문
      Spring Data Redis
      Springframework
      자바기초
      Til
      HTML
      자바스크립트
      JS
      백준
      springsecurity
      자바 면접
      리눅스마스터2급
      spring redis
      자바 알고리즘
      레디스
      jsp기초
      자바 반복문
      스프링프레임워크
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.3
    공부하고 기억하는 공간
    Redis Pub/Sub을 활용한 채팅 구현의 여정 - Chat Domain(Entity ~ Controller)

    개인정보

    • 티스토리 홈
    • 포럼
    • 로그인
    상단으로

    티스토리툴바

    단축키

    내 블로그

    내 블로그 - 관리자 홈 전환
    Q
    Q
    새 글 쓰기
    W
    W

    블로그 게시글

    글 수정 (권한 있는 경우)
    E
    E
    댓글 영역으로 이동
    C
    C

    모든 영역

    이 페이지의 URL 복사
    S
    S
    맨 위로 이동
    T
    T
    티스토리 홈 이동
    H
    H
    단축키 안내
    Shift + /
    ⇧ + /

    * 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.