Querydsl을 사용하면서 복잡한 쿼리프로젝션 문제 해결하기

SionBackEnd·2023년 4월 4일
0

Spring(봄)

목록 보기
21/22

도입

Querydsl을 사용하면서 생성자 + 쿼리프로젝션을 사용해보기로 하였다. 하지만 DTO의 구조는 그저 필드값만 나열되어 있는것이 아니라 그안에 다른 DTO가 존재했다.

    @Getter
    @Builder
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public static class ResponseSimpleFriend {
        UserDto.ResponseDetailUser user;
        private Integer itemCount;

        @QueryProjection
        public ResponseSimpleFriend(UserDto.ResponseDetailUser user, Integer itemCount) {
            this.user = user;
            this.itemCount = itemCount;
        }
    }

ResponseDetailUser 클래스에는 상세한 유저의 정보가 들어있고 당연히 친구리스트를 반환하기 위해서 필요한 DTO였다.

 @Override
    public Page<FriendDto.ResponseSimpleFriend> getFriendList(Long userId, FriendStatus friendStatus, Boolean isBirthday, Pageable pageable) {
        List<FriendDto.ResponseSimpleFriend> result = queryFactory.select(new QFriendDto_ResponseSimpleFriend(
                        new QUserDto_ResponseDetailUser(friend.userFriend.id,
                                friend.userFriend.name,
                                friend.userFriend.phone,
                                friend.userFriend.birthday,
                                friend.userFriend.infoMessage,
                                new QImageDto_SimpleImageDto(friend.userFriend.image.id, friend.userFriend.image.remotePath)),
                        friend.userFriend.itemUserList.size()))
                .from(friend)
                .where(friend.user.id.eq(userId)
                                .and(friend.user.userStatus.eq(UserStatus.ACTIVE)),
                        birthday(isBirthday),
                        status(friendStatus))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(friend.user.name.asc())
                .fetch();

        Long count = queryFactory
                .select(friend.count())
                .from(friend)
                .where(friend.user.id.eq(userId)
                                .and(friend.user.userStatus.eq(UserStatus.ACTIVE)),
                        birthday(isBirthday),
                        status(friendStatus))
                .fetchOne();

        return new PageImpl<>(result, pageable, Objects.requireNonNull(count));
    }

위 코드를 확인하면 쿼리프로젝션안에 다른 프로젝션이 들어있다. 나름 코드를 잘 작성했다고 생각했고 테스트를 진행해본결과 이상하게 result 값 안에 아무것도 들어있지 않았다...

  @BeforeEach
    public void init() {
        User user1 = User.builder()
                .email(EMAIL)
                .name(NAME)
                .phone(PHONE)
                .birthday(LocalDate.now())
                .build();

        User user2 = User.builder()
                .email(DIFF_EMAIL)
                .name(DIFF_NAME)
                .phone(DIFF_PHONE)
                .birthday(LocalDate.of(2023, 4, 3))
                .build();
        Friend friend1 = Friend.builder()
                .user(user1)
                .userFriend(user2)
                .build();
        Friend friend2 = Friend.builder()
                .user(user2)
                .userFriend(user1)
                .build();

        user1.getFriendList().add(friend1);
        user2.getFriendList().add(friend2);

        em.persist(user1);
        em.persist(user2);
        em.persist(friend1);
        em.persist(friend2);
    }

    @Test
    @DisplayName("동적 친구 리스트 검색 성공")
    public void getFriendList_suc() {
        //given
        User user = userRepository.findUserByPhone(PHONE).get();
        //when
        Page<FriendDto.ResponseSimpleFriend> friendList = friendRepository.getFriendList(user.getId(), FriendStatus.ACTIVE, false, PAGE_REQUEST);
        //then
        assertThat(friendList.getSize()).isEqualTo(2);
        assertThat(friendList.getTotalPages()).isEqualTo(1);
        assertThat(friendList.getContent().size()).isEqualTo(1);
    }

이유가 무엇인지 파악하려고 많은 구글링과 김영한님 강의를 봐보았지만, 딱히 이렇게 사용하는 사람들이 없었고 나와 같은 고민을 한 사람들이 없었다.(아니면 나의 검색어가 문제였을수도..)

고민

문제가 무엇일지 곰곰히 생각해 보았다. 많은 시도도 진행했고, 생각에 생각을 거듭한 결과 아주 중요한 것을 하지 않았기 때문에 테스트가 통과하지 않았던것을 깨닫게 되었다.

이미 눈치채신 분도 계시겠지만, join을 진행하지 않아서 데이터를 가져오지 못한것이다. 더 정확히 말하면 userFriend와 ManytoOne관계로 LazyLoding으로 설정되어있어서 쿼리가 데이터를 찾지 못한것이라고 판단된다.

지나고 보니 매우 어처구니 없는 실수를 해버렸다. 뭐에 홀렸었는지.. 하지만 오늘도 깨달은건 코드는 거짓말을 하지 않는다는 것이다.

해결

@Override
    public Page<FriendDto.ResponseSimpleFriend> getFriendList(Long userId, FriendStatus friendStatus, Boolean isBirthday, Pageable pageable) {
        List<FriendDto.ResponseSimpleFriend> result = queryFactory.select(new QFriendDto_ResponseSimpleFriend(
                        new QUserDto_ResponseDetailUser(friend.userFriend.id,
                                friend.userFriend.name,
                                friend.userFriend.phone,
                                friend.userFriend.birthday,
                                friend.userFriend.infoMessage,
                                new QImageDto_SimpleImageDto(friend.userFriend.image.id,
                                        friend.userFriend.image.remotePath)),
                        friend.userFriend.itemUserList.size()))
                .from(friend)
                .leftJoin(friend.userFriend.image, QImage.image)
                .leftJoin(friend.userFriend, QUser.user)
                .leftJoin(friend.user, QUser.user)
                .where(friend.user.id.eq(userId)
                                .and(friend.user.userStatus.eq(UserStatus.ACTIVE)),
                        birthday(isBirthday),
                        status(friendStatus))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(friend.user.name.asc())
                .fetch();

        Long count = queryFactory
                .select(friend.count())
                .from(friend)
                .where(friend.user.id.eq(userId)
                                .and(friend.user.userStatus.eq(UserStatus.ACTIVE)),
                        birthday(isBirthday),
                        status(friendStatus))
                .fetchOne();

        return new PageImpl<>(result, pageable, count);
    }

    private BooleanExpression birthday(Boolean isBirthday) {
        return isBirthday ? friend.userFriend.birthday.eq(LocalDate.now()) : null;
    }

    private BooleanExpression status(FriendStatus friendStatus) {
        return friend.friendStatus.eq(friendStatus);
    }
}

생성자 + 쿼리프로젝션을 사용하는데 훨씬 더 편리해졌다. 한번에 데이터를 조회하기 때문에 가독성 또한 더욱 좋다졌다. 한방쿼리를 작성하기 때문에 카운트 쿼리와 토큰 검증 쿼리 를 포함해서도 3번이면 조회가 완료된다. 추후 대량의 더미데이터를 넣고 테스트할때 처리속도가 너무 기대된다.

profile
많은 도움 얻어가시길 바랍니다!

0개의 댓글