Project/Nuwa

Nuwa Project - 채팅방 조회 (마지막 채팅 값을 넣어서 조회)

Llimy1 2024. 2. 21. 20:17
반응형
SMALL
반응형
SMALL

다이렉트 채팅방을 조회를 하는 로직을 작성했습니다.

API 반환을 할 때 채팅방 정보와 마지막 채팅 그리고 마지막 채팅의 생성된 시간을 반환하려고 했습니다.

처음엔 이 난관을 어떤 방식으로 극복을 해야하나 싶었습니다.

채팅방을 생성한 시간 순으로 조회를 하는 것은 Spring Data를 사용하여 페이징도 간편하게 작성이 가능했습니다.

또한 MongoRepository를 사용해도 Spring Data Jpa와 동일하게 

ListPagingAndSortingRepository<T, ID>

위의 부분을 상속을 받고 있기에 채팅 내역 또한 페이징을 간편하게 가능했습니다.

 

여기서 서로 각각 조회를 진행하고 dto로 각각 넘겨주면 되지 않나? 라고 생각을 했습니다.

그래서 처음엔 모든 데이터를 다 가져와 

int i = 0;
for (Direct direct : directSliceContent) {

    given(directMessageQueryService.countUnReadMessage(anyString(), anyString()))
            .willReturn(unReadCount);

    if (directMessageSlice.hasContent()) {
        DirectMessage directMessage = directMessageSlice.getContent().get(i);

        DirectChannelResponseDto directChannelResponseDto = DirectChannelResponseDto.builder()
                .roomId(direct.getRoomId())
                .name(direct.getName())
                .workSpaceId(direct.getId())
                .createMemberName(direct.getCreateMember().getName())
                .joinMemberName(direct.getJoinMember().getName())
                .unReadCount(unReadCount)
                .lastMessage(directMessage.getContent())
                .createdAt(directMessage.getCreatedAt())
                .build();
        directChannelResponseDtoList.add(directChannelResponseDto);
        i++;
    }
}

 

해당 방식과 같이 for 루프를 돌면서 dto에 넣어줘서 맵핑을 했습니다.

여기서 저는 데이터를 헤당 roomId에 맞게 넣어서 채팅방과 같이 반환을 해줘야하는데

i의 값을 하나씩 증가를 시키며 넣는다면 데이터가 제대로 불러오지 않는다면

결국 채팅방에 맞는 데이터가 들어가지 않게 된다고 생각을 했습니다.

 

그러면 여기서 어떻게 진행을 해볼까하다 페이징을 사용해서 채팅방 데이터를 가져와서

for 루프를 돌면서 채팅 데이터를 하나씩 맵핑을 하면 되지 않을까?

이런 생각을 했습니다.

 

머리 속에 페이징에 대한 생각만 가득하다보니 마지막 채팅 데이터를 불러오는 것도 페이징으로 처리를 했습니다.

채팅 데이터는 많은 양의 데이터가 있어서 페이징 처리를 해서 지정된 개수만 가져오는 것이 좋다고 생각을 했습니다.

 

Slice<DirectMessage> findDirectMessageByRoomIdOrderByCreatedAtDesc(String roomId, Pageable pageable);

 

다음과 같이 채팅 데이터를 페이징으로 불러오는 메소드를 작성하고

directChannelByWorkSpaceId.forEach(direct -> {
    PageRequest pageRequest = PageRequest.of(0, 1);

    Long unReadCount = directMessageQueryService.countUnReadMessage(direct.getRoomId(), email);

    // 마지막 채팅과 시간 가져오기
    Slice<DirectMessage> directMessageByRoomIdOrderByCreatedAt =
            directMessageRepository.findDirectMessageByRoomIdOrderByCreatedAtDesc(direct.getRoomId(), pageRequest);

    if (directMessageByRoomIdOrderByCreatedAt.hasContent()) {
        DirectMessage directMessage =
                directMessageByRoomIdOrderByCreatedAt.getContent().get(0);

        DirectChannelResponseDto directChannelResponseDto = DirectChannelResponseDto.builder()
                .roomId(direct.getRoomId())
                .name(direct.getName())
                .workSpaceId(direct.getId())
                .createMemberName(direct.getCreateMember().getName())
                .joinMemberName(direct.getJoinMember().getName())
                .unReadCount(unReadCount)
                .lastMessage(directMessage.getContent())
                .createdAt(directMessage.getCreatedAt())
                .build();
        directChannelResponseDtoList.add(directChannelResponseDto);
    }
});

 

for 루프를 돌면서 생성 시간의 역순 -> 즉 최근 생성된 데이터 순으로 데이터를 가져와

가장 첫 페이지의 0번째 index 값을 가져와서 해당 채팅방에 맞는 마지막 데이터를 가져왔습니다.

처음 사용했던 로직 for 루프를 돌면서 i 값을 증가시켜 데이터를 넣는 것과 두번째 방식과 동일하지 않나
생각이 들 수도 있는데 저도 생각을 하면서 어? 정말 그런거 아닌가 생각이 들었지만
두번째 방법은 데이터를 roomId에 맞게 가져와 하나하나 맵핑을 해주기에
해당 부분 첫번째 방식의 문제점을 해결한 부분이라 생각합니다.

 

물론 해당 방식을 제외하고 더 좋은 방식이 있을 것 같다고 생각이 들긴 하지만 일단 불필요한 데이터는 최소화 했다고 생각합니다.

추후 리팩토링을 진행을 하게 되면 더 좋은 방식을 생각해서 들고 오겠습니다.


변경 사항

테스트 코드를 작성하고 무언가 쎄한 느낌이 들기 시작했습니다.

채팅방 리스트를 가져올 때 워크스페이스 별로 채팅방 리스트를 가져오는데

다이렉트 채널은 모두가 다른 인원의 채팅을 보면 안되겠네?란 생각이 들었고 현재 채팅방 리스트를 가져오는 부분을 확인하니

Slice<Direct> findDirectChannelByWorkSpaceId(Long WorkSpaceId, Pageable pageable);

해당 워크스페이스에 모든 다이렉트 채널 (채팅방)을 가져오도록 되어 있었습니다.

 

이 부분을 수정하기 위해 여러 생각을 해보았고 다이렉트 채팅은 결국 1:1 채팅이고

내가 생성을 한 채팅 또는 참여 중인 채팅방을 전부 가져와야 된다고 생각이 들었습니다.

Slice<Direct> findDirectChannelByCreateMemberIdOrJoinMemberId(Long workSpaceMemberId, Pageable pageable);

 

JPA를 이용하여 해당 조건에 맞게 모든 채팅채널 리스트를 가져올 수 있었습니다.

 

그리고 테스트 코드를 작성하면서 로직 변화가 있었습니다. 현재 record를 DTO로 사용하면서 로직 구현을 하고 있었는데

record는 데이터를 운반 객체를 생성을 간편화를 위한 특수한 클래스입니다.

@Setter 어노테이션을 사용을 하지 못하게 되었고 저는 채팅방 정보를 DTO에 먼저 builder를 사용하여 맵핑을 하고

페이징으로 가져온 마지막 메세지와 시간을 맵핑을 해주기 위해 DTO를 record에서 기본 클래스로 변경을 했습니다.

 

import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.time.LocalDateTime;

@Getter
@ToString
@EqualsAndHashCode
public class DirectChannelResponseDto {

    private String roomId;
    private String name;
    private Long workSpaceId;
    private String createMemberName;
    private String joinMemberName;
    private Long unReadCount;

    @Setter
    private String lastMessage;
    @Setter
    private LocalDateTime createdAt;

    @Builder
    public DirectChannelResponseDto(String roomId, String name, Long workSpaceId, String createMemberName, String joinMemberName, Long unReadCount) {
        this.roomId = roomId;
        this.name = name;
        this.workSpaceId = workSpaceId;
        this.createMemberName = createMemberName;
        this.joinMemberName = joinMemberName;
        this.unReadCount = unReadCount;
    }
}

 

추가로 서비스 로직도 억지로 데이터를 맵핑을 하려고 하니 for 루프 내부에서 같은 데이터가 채팅방 개수만큼 들어가는 부분도 있었습니다.

public DirectChannelListResponseDto directChannelSliceSortByMessageCreateDateDesc(String email, Long workSpaceId, Pageable pageable) {

    List<DirectChannelResponseDto> directChannelResponseDtoList = new ArrayList<>();

    WorkSpaceMember findWorkSpaceMember = workSpaceMemberRepository.findByMemberEmailAndWorkSpaceId(email, workSpaceId)
            .orElseThrow(() -> new NotFoundException(WORK_SPACE_MEMBER_NOT_FOUND));

    Long findWorkSpaceMemberId = findWorkSpaceMember.getId();

    // 워크스페이스 리스트 가져오기 -> 내가 생성을 한 또는 내가 참여를 한 채팅방 리스트 가져오기 (생성 시간 별로 나눌 필요가 없음 -> 마지막 채팅의 시간 순으로 정렬을 해야함)
    Slice<Direct> directChannelList =
            directChannelRepository.findDirectChannelByCreateMemberIdOrJoinMemberId(findWorkSpaceMemberId, pageable);

    // 리스트를 순회하면서 해당 roomId에 맞는 마지막 채팅과 시간 가져오기
    directChannelList.forEach(direct -> {

        PageRequest pageRequest = PageRequest.of(0, 1);

        Long unReadCount = directMessageQueryService.countUnReadMessage(direct.getRoomId(), email);

        DirectChannelResponseDto directChannelResponseDto = DirectChannelResponseDto.builder()
                .roomId(direct.getRoomId())
                .name(direct.getName())
                .workSpaceId(direct.getId())
                .createMemberName(direct.getCreateMember().getName())
                .joinMemberName(direct.getJoinMember().getName())
                .unReadCount(unReadCount)
                .build();

        // 마지막 채팅과 시간 가져오기
        Slice<DirectMessage> directMessageByRoomIdOrderByCreatedAt =
                directMessageRepository.findDirectMessageByRoomIdOrderByCreatedAtDesc(direct.getRoomId(), pageRequest);

        if (directMessageByRoomIdOrderByCreatedAt.hasContent()) {
            DirectMessage directMessage =
                    directMessageByRoomIdOrderByCreatedAt.getContent().get(0);

            directChannelResponseDto.setLastMessage(directMessage.getContent());
            directChannelResponseDto.setCreatedAt(directMessage.getCreatedAt());
        }

        directChannelResponseDtoList.add(directChannelResponseDto);
    });

    // 해당 DTO에 맵핑된 생성 시간으로 재정렬하여 최근 메세지 순으로 채팅방 정렬
    List<DirectChannelResponseDto> sortByCreatedAtResponseList = directChannelResponseDtoList.stream()
            .sorted(Comparator.comparing(DirectChannelResponseDto::getCreatedAt).reversed())
            .toList();

    // 페이징 정보 추가
    boolean hasNext = directChannelList.hasNext();
    int currentPage = directChannelList.getNumber();
    int pageSize = directChannelList.getSize();

    return DirectChannelListResponseDto.builder()
            .directChannelResponseDtoList(sortByCreatedAtResponseList)
            .hasNext(hasNext)
            .currentPage(currentPage)
            .pageSize(pageSize)
            .build();
}

 

마지막으로 채팅 메세지에서 가져온 시간 순으로 리스트 재정렬을 진행하고 채팅방 페이징 정보까지 추가를 해줘서 로직을 마무리 했습니다.

 

@Test
@DisplayName("[Service] Direct Channel Slice Sort By Message CreateDate")
void directChannelSliceSortByMessageCreateDate() {
    //given
    String roomId1 = "roomId1";
    String content1 = "content1";
    Long readCount = 0L;
    String email = "abcd@gmail.com";

    given(workSpaceMemberRepository.findByMemberEmailAndWorkSpaceId(anyString(), any()))
            .willReturn(Optional.of(senderWorkSpaceMember));

    DirectMessage directMessage1 = DirectMessage.createDirectMessage(workSpaceId, roomId1, sender.getId(), content1, readCount);

    ReflectionTestUtils.setField(directMessage1, "id", UUID.randomUUID().toString());
    ReflectionTestUtils.setField(directMessage1, "createdAt", LocalDateTime.now());

    List<DirectChannelResponseDto> directChannelResponseDtoList = new ArrayList<>();

    Direct direct1 = Direct.createDirectChannel(workSpace, senderWorkSpaceMember, receiverWorkSpaceMember);

    ReflectionTestUtils.setField(direct1, "createdAt", LocalDateTime.now());

    List<Direct> directList = new ArrayList<>(List.of(direct1));


    List<DirectMessage> directMessageList = new ArrayList<>(List.of(directMessage1));

    Slice<Direct> directSlice = new SliceImpl<>(directList, pageRequest, false);

    given(directChannelRepository.findDirectChannelByCreateMemberIdOrJoinMemberId(any(), any()))
            .willReturn(directSlice);

    Long unReadCount = 10L;

    directSlice.forEach(direct -> {

        given(directMessageQueryService.countUnReadMessage(anyString(), anyString()))
                .willReturn(unReadCount);

        PageRequest pageRequest1 = PageRequest.of(0, 1);

        Slice<DirectMessage> directMessageSlice = new SliceImpl<>(directMessageList, pageRequest1, false);

        given(directMessageRepository.findDirectMessageByRoomIdOrderByCreatedAtDesc(anyString(), any()))
                .willReturn(directMessageSlice);

        DirectChannelResponseDto directChannelResponseDto = DirectChannelResponseDto.builder()
                .roomId(direct.getRoomId())
                .name(direct.getName())
                .workSpaceId(direct.getId())
                .createMemberName(direct.getCreateMember().getName())
                .joinMemberName(direct.getJoinMember().getName())
                .unReadCount(unReadCount)
                .build();

        if (directMessageSlice.hasContent()) {
            DirectMessage directMessage = directMessageSlice.getContent().get(0);

            directChannelResponseDto.setLastMessage(directMessage.getContent());
            directChannelResponseDto.setCreatedAt(directMessage.getCreatedAt());
        }
        directChannelResponseDtoList.add(directChannelResponseDto);
    });

    List<DirectChannelResponseDto> sortByCreatedAtResponseList = directChannelResponseDtoList.stream()
            .sorted(Comparator.comparing(DirectChannelResponseDto::getCreatedAt).reversed())
            .toList();

    boolean hasNext = directSlice.hasNext();
    int currentPage = directSlice.getNumber();
    int pageSize = directSlice.getSize();

    DirectChannelListResponseDto mockDirectChannelListResponseDto = DirectChannelListResponseDto.builder()
            .directChannelResponseDtoList(sortByCreatedAtResponseList)
            .hasNext(hasNext)
            .currentPage(currentPage)
            .pageSize(pageSize)
            .build();


    //when
    DirectChannelListResponseDto directChannelListResponseDto =
            directChannelService.directChannelSliceSortByMessageCreateDateDesc(email, workSpaceId, pageRequest);


    //then
    assertThat(directChannelListResponseDto).isNotNull();
    assertThat(directChannelListResponseDto.directChannelResponseDtoList()).
            containsAll(mockDirectChannelListResponseDto.directChannelResponseDtoList());
    assertThat(directChannelListResponseDto.directChannelResponseDtoList().get(0).getCreatedAt())
            .isEqualTo(mockDirectChannelListResponseDto.directChannelResponseDtoList().get(0).getCreatedAt());
    assertThat(directChannelListResponseDto.currentPage()).isEqualTo(mockDirectChannelListResponseDto.currentPage());
    assertThat(directChannelListResponseDto.hasNext()).isEqualTo(mockDirectChannelListResponseDto.hasNext());
    assertThat(directChannelListResponseDto.pageSize()).isEqualTo(mockDirectChannelListResponseDto.pageSize());
}

 

추가로 테스트 코드를 작성을 했을 때 containsAll이 계속해서 오류가 발생을 했습니다.

내부 값은 동일한데 일치하지 않는다? 이게 무슨 말이지 고민을 하던 도중

record 클래스는 기본적으로 Equals 와 HashCode가 들어가 있습니다.

그런데 제가 변경한 DTO는 EqualsAndHashCode가 적용이 되지 않았기에 해당 참조 값이 동일하지 않아 나타나는 문제였습니다.

@EqualsAndHashCode

해당 어노테이션을 DTO에 붙여주니 정상적으로 테스트가 통과를 했습니다.

반응형
LIST