[이전글]
이전 글에는 Redis와 채팅을 사용하기 위한 Config파일들을 알아보았다.
https://sunro1994.tistory.com/254
[소개]
이전 글에서는 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)을 알아보겠다. 함께 알아보는 이유는 유저가 미접속시 알림을 보내는 기능이 추가되어 있기 때문이다.
'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 |