QueryDsl로 검색 필터 적용하기

이의찬·2023년 7월 6일
0

Springboot

목록 보기
3/12

https://github.com/Eui9179/Mission_LeeEuiChan/tree/main

문제상황

미션 프로젝트인 그램그램을 진행하면서 나를 좋아하는 사람들의 필터링 기능을 적용해야 했다.

총 세가지 기능을 추가해야 한다.

  • 내가 받은 호감리스트에서 성별 필터링기능 구현
  • 내가 받은 호감리스트에서 호감사유 필터링기능 구현
  • 내가 받은 호감리스트에서 정렬기능

https://localhost/usr/likeablePerson/toList?

gender=M&

attractiveTypeCode=&

sortCode=1

구현

Request DTO

쿼리 파라미터로 넘어오기 때문에 @ModelAttribute로 리퀘스트 DTO를 만들어 주었다.

@Setter
@Getter
public class SearchFilter {
    private String gender;
    private Integer attractiveTypeCode;
    private Integer sortCode = 1; // 기본값
}

기본적으로 sortCode의 기본값이 1이기 때문에 넘어오지 않아도 1로 설정되게 하였다.

Controller

@PreAuthorize("isAuthenticated()")
@GetMapping("/toList")
public String showToList(Model model, SearchFilter searchFilter) {
    InstaMember instaMember = rq.getMember().getInstaMember();

    // 인스타인증을 했는지 체크
    if (instaMember != null) {
        // 해당 인스타회원이 좋아하는 사람들 목록
        RsData likeablePeople = likeablePersonService.findByToInstaMemberWithFilter(
                instaMember, searchFilter.gender, searchFilter.attractiveTypeCode, searchFilter.sortCode
        );
        model.addAttribute("likeablePeople", likeablePeople.getData());
    }

    return "usr/likeablePerson/toList";
}

Controller에서 @ModelAttribute로 SearchFilter를 받았다.

💡 @ModelAttribute vs Map

검색 조건이 추가되면 DTO와 Controller, Service 등 코드를 수정해야되는 부분이 많은데 Map으로 받아서 처리하는 게 좋을까?

Service

public RsData<List<LikeablePerson>> findByToInstaMemberWithFilter(
        InstaMember toInstaMember, String gender, Integer attractiveTypeCode, int sortCode) {

    List<LikeablePerson> likeablePeople = likeablePersonRepository
            .findQslByToInstaMemberWithFilter(toInstaMember, gender, attractiveTypeCode, sortCode);
    return RsData.of("S-1", "", likeablePeople);
}

sortCode는 null을 허용하지 않기 위해 int로 설정하였다.

QueryDsl

  1. Repository에 상속할 interface를 만들어야한다.
public interface LikeablePersonRepositoryCustom {
    Optional<LikeablePerson> findQslByFromInstaMemberIdAndToInstaMember_username(long fromInstaMemberId, String toInstaMemberUsername);

    List<LikeablePerson> findQslByToInstaMemberWithFilter(
            InstaMember toInstaMember, String gender, Integer attractiveTypeCode, int sortCode);
}
  1. 다음으로 인터페이스를 구현해야 한다.
@Override
public List<LikeablePerson> findQslByToInstaMemberWithFilter(
        InstaMember toInstaMember, String gender, Integer attractiveTypeCode, int sortCode) {
    if (toInstaMember == null) {
        return null;
    }

    return jpaQueryFactory
            .selectFrom(likeablePerson)
            .where(
                    eqToInstaMember(toInstaMember),
                    eqGender(gender),
                    eqAttractiveTypeCode(attractiveTypeCode)
            )
            .orderBy(resolveSortCode(sortCode))
            .fetch();
}
  1. 필터 기능 - 성별, 호감사유

.where() and로 처리하였는데 eqToInstaMember()eqGender(), eqAttractiveTypeCode()가 null이라면 쿼리의 조건을 처리하지 않고 빼는 특성을 사용하여 구현하였다.

아래 코드를 보면 BooleanExpression 만약 null이거나 빈 값으면 null을 리턴하여서 조건에서 제외되게 하였다.

@RequiredArgsConstructor
@Slf4j
public class LikeablePersonRepositoryImpl implements LikeablePersonRepositoryCustom {
    private final JPAQueryFactory jpaQueryFactory;
		...생략

    private BooleanExpression eqToInstaMember(InstaMember toInstaMember) {
        return likeablePerson.toInstaMember.eq(toInstaMember);
    }

    private BooleanExpression eqGender(String gender) {
        if (gender == null || gender.equals("")) {
            return null;
        }
        return likeablePerson.fromInstaMember.gender.eq(gender);
    }

    private BooleanExpression eqAttractiveTypeCode(Integer attractiveTypeCode) {
        if (attractiveTypeCode == null) {
            return null;
        }
        return likeablePerson.attractiveTypeCode.eq(attractiveTypeCode);
    }
		...생략
}
  1. 정렬 기능
@RequiredArgsConstructor
@Slf4j
public class LikeablePersonRepositoryImpl implements LikeablePersonRepositoryCustom {
    private final JPAQueryFactory jpaQueryFactory;
		...생략

    private OrderSpecifier[] resolveSortCode(Integer sortCode) {
        List<OrderSpecifier> orderSpecifiers = new ArrayList<>();
        switch (sortCode) {
            case 2 -> orderSpecifiers.add(likeablePerson.modifyDate.asc());
            case 3 -> orderSpecifiers.add(likeablePerson.fromInstaMember.toLikeablePeople.size().desc());
            case 4 -> orderSpecifiers.add(likeablePerson.fromInstaMember.toLikeablePeople.size().asc());
            case 5 -> {
                orderSpecifiers.add(likeablePerson.fromInstaMember.gender.desc());
                orderSpecifiers.add(likeablePerson.modifyDate.desc());
            }
            case 6 -> {
                orderSpecifiers.add(likeablePerson.attractiveTypeCode.asc());
                orderSpecifiers.add(likeablePerson.modifyDate.desc());
            }
            default -> orderSpecifiers.add(likeablePerson.modifyDate.desc());
        }
        return orderSpecifiers.toArray(new OrderSpecifier[orderSpecifiers.size()]);
    }
}

성별과 호감사유로 정렬할 경우 최신순으로 같이 정렬해야 하기때문에 .orderBy(resolveSortCode(sortCode))에 OrderSpectifier[] 다중 정렬 조건을 넣어 처리하였다.

테스트

기존에 존재했던 데이터로 성별, 호감사유 필터와 인기도 많은 순으로 정렬하는 테스트 코드를 작성하였다.

필터링 - 성별

@Test
@DisplayName("필터링 기능 테스트 - 성별")
void t013() {
    //given
    String gender = "W";
    String instaUserName = "insta_user6";
    InstaMember toInstaMember = instaMemberService.findByUsername(instaUserName)
            .orElseThrow(() -> new RuntimeException("데이터가 없습니다. NotProd.java 참조"));

    //when
    List<LikeablePerson> likeablePeople = likeablePersonService
            .findByToInstaMemberWithFilter(toInstaMember, gender, null, 1)
            .getData();

    //then
    assertThat(likeablePeople.size()).isEqualTo(2);
}

기존 데이터에 나를 좋아하는 사람이 남자 1명, 여자 2명으로 설정되어 있으므로 여자를 넣었을 때, 두명이 나와야 한다.


필터링 - 호감사유

@Test
@DisplayName("필터링 기능 테스트 - 호감사유")
void t014() {
    //given
    Integer attractiveTypeCode = 1;
    String instaUserName = "insta_user6";
    InstaMember toInstaMember = instaMemberService.findByUsername(instaUserName)
            .orElseThrow(() -> new RuntimeException("데이터가 없습니다. NotProd.java 참조"));

    //when
    List<LikeablePerson> likeablePeople = likeablePersonService
            .findByToInstaMemberWithFilter(toInstaMember, null, attractiveTypeCode, 1)
            .getData();

    //then
    likeablePeople.forEach(
            lp -> assertThat(lp.getAttractiveTypeCode()).isEqualTo(attractiveTypeCode)
    );

}

기존 데이터에 호감사유가 1인 유저가 2명 있었고 1로 필터링 하였을 때 모든 데이터가 호감사유가 1이어야 한다.


정렬 - 인기도

@Test
@DisplayName("정렬 기능 테스트 - 정렬 - 인기도 많은 순")
void t015() {
	//given
	ToListSearchForm toListSearchForm = ToListSearchForm.builder()
                .gender(null)
                .attractiveTypeCode(null)
                .sortCode(3)
                .build();

	//when
    List<LikeablePerson> likeablePeople = likeablePersonService
                .findByToInstaMemberWithFilter(toInstaMember, toListSearchForm)
                .getData();

	//then
    assertThat(likeablePeople).isSortedAccordingTo(
                Comparator.comparing(e -> e.getFromInstaMember().getLikesCount(), Comparator.reverseOrder())
        );
    }

나를 좋아하는 유저의 호감 표시를 받은 개수로 정렬하였다. Comparator의 comparing()을 사용하여서 정렬이 되었는지 테스트하였다.


인기도 적은 순
인기 많은 순과 마찬가지로 역순을 테스트하였다.

@Test
@DisplayName("정렬 기능 테스트 - 정렬 - 인기도 적은 순")
void t016() {
	//given
    ToListSearchForm toListSearchForm = ToListSearchForm.builder()
                .gender(null)
                .attractiveTypeCode(null)
                .sortCode(4)
                .build();

	//when
	List<LikeablePerson> likeablePeople = likeablePersonService
                .findByToInstaMemberWithFilter(toInstaMember, toListSearchForm)
                .getData();

	//then
    assertThat(likeablePeople).isSortedAccordingTo(
                Comparator.comparing(e -> e.getFromInstaMember().getLikesCount())
        );
    }

이외에도 호감사유 정렬, 성별 정렬, querydsl 테스트 등 진행하였다.

0개의 댓글