QueryDSL을 사용한 효과적인 동적쿼리

wellbeing-dough·2024년 1월 11일

문제 상황

  1. 우리는 스터디 게시글을 조회할때 페이지네이션을 사용하여 10개씩 조회한다

  2. 해당 화면을 보는 유저(인가된 유저)가 이 화면을 보는지 안보는지 확인하려고 기존에는 isBookmarked라는 api를 따로 사용했다

  1. 그랬더니 페이지네이션으로 10개 조회할때마다 isBookmarked api를 10번 보내는게 아닌가... preflight까지 20번이다 ㄷㄷ...

  1. 이제 게시글 조회만으도 인가가 필요하다(화면을 보는 유저가 누군지 알아야하기 때문에) 하지만 우리는 비회원도 게시글은 볼 수 있는 서비스이다

해결

1. jwt가 안들어오면 인가 전에 userId에 비회원 반영

    @Override
    public UserId resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                                    NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        try {
            String jwtToken = webRequest.getHeader(JwtProperties.ACCESS_HEADER_STRING);
            if (jwtToken == null) {
                return new UserId(null);
            }
            jwtToken = jwtToken.replace(JwtProperties.TOKEN_PREFIX, "");
            Long id = (JWT.require(Algorithm.HMAC512(SECRET)).build().verify(jwtToken).getClaim("id")).asLong();
            return new UserId(id);
        } catch (Exception e) {
            return null;
        }
    }

우리는 리졸버에서 인가를 처리한다 그래서 비회원을 위해서 jwt가 없더라도 null을 보내는 쪽으로 하였다

2. 동적 쿼리를 사용하여 해결

@Getter
public class PostDataByInquiry {

    private Long postId;
    private MajorType major;
    private String title;
    private LocalDate studyStartDate;
    private LocalDate studyEndDate;
    private LocalDateTime createdDate;
    private Integer studyPerson;
    private GenderType filteredGender;
    private Integer penalty;
    private String penaltyWay;
    private Integer remainingSeat;
    private boolean close;
    private boolean isBookmarked;	//추가
    private UserData userData;

    @Builder
    public PostDataByInquiry(Long postId, MajorType major, String title, LocalDate studyStartDate, LocalDate studyEndDate, LocalDateTime createdDate, Integer studyPerson, GenderType filteredGender, Integer penalty, String penaltyWay, Integer remainingSeat, boolean close, boolean isBookmarked, UserData userData) {
        this.postId = postId;
        this.major = major;
        this.title = title;
        this.studyStartDate = studyStartDate;
        this.studyEndDate = studyEndDate;
        this.createdDate = createdDate;
        this.studyPerson = studyPerson;
        this.filteredGender = filteredGender;
        this.penalty = penalty;
        this.penaltyWay = penaltyWay;
        this.remainingSeat = remainingSeat;
        this.close = close;
        this.isBookmarked = isBookmarked;
        this.userData = userData;
    }
}

이렇게 isBookmarked를 dto에 추가 하였다

    @Override
    public List<PostDataByInquiry> findByInquiry(final InquiryRequest inquiryRequest, final Pageable pageable, Long userId) {
        JPAQuery<PostDataByInquiry> data = jpaQueryFactory
                .select(Projections.constructor(PostDataByInquiry.class,
                        studyPostEntity.id.as("postId"), studyPostEntity.major, studyPostEntity.title, studyPostEntity.studyStartDate, studyPostEntity.studyEndDate,
                        studyPostEntity.createdDate, studyPostEntity.studyPerson, studyPostEntity.filteredGender,
                        studyPostEntity.penalty, studyPostEntity.penaltyWay, studyPostEntity.remainingSeat, studyPostEntity.close,
                        bookmarkPredicate(userId),	//여기서 isBookmarked 값 지정해준다
                        Projections.constructor(
                                UserData.class,
                                userEntity.id, userEntity.major, userEntity.nickname, userEntity.imageUrl
                        )
                ))
                .from(studyPostEntity)
                .leftJoin(userEntity).on(studyPostEntity.postedUserId.eq(userEntity.id))
                .where(textEq(inquiryRequest.getInquiryText()).or(majorEq(inquiryRequest.getInquiryText(), inquiryRequest.isTitleAndMajor())))
                .orderBy(hotPredicate(inquiryRequest), studyPostEntity.createdDate.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize() + 1);

        if (userId != null) {
            data.leftJoin(bookmarkEntity).on(studyPostEntity.id.eq(bookmarkEntity.postId).and(bookmarkEntity.userId.eq(userId)));
        }		//유저 아이디가 있으면 그떄서야 bookmark와 join함

        return data.fetch();
    }
    
    private BooleanExpression bookmarkPredicate(Long userId) {
        if (userId != null) {
            return Expressions.booleanTemplate("{0} = {1}", bookmarkEntity.userId, userId);
        }
        return Expressions.asBoolean(Expressions.constant(false));
    }

유저 아이디가 있으면 그떄서야 bookmark와 join한다
bookmarkPredicate 메서드는 userId를 인자로 받아서 BooleanExpression을 반환한다 QueryDSL에서 조건문을 동적으로 생성했다

QueryDSL에서는 BooleanExpression이라는 타입을 통해 where 절의 조건을 표현하는데 이 메서드는 특정 게시물이 사용자에 의해 북마크되었는지 여부를 판단하는 조건을 생성하는 역할을 한다

코드를 보면 현재 조회하고 있는 북마크의 userId가 인자로 받은 userId와 같은지를 판단하는 조건을 생성한다

만약 비회원이 조회한다면(userId가 null인 경우) Expressions.asBoolean(Expressions.constant(false))를 통해 항상 false를 반환하는 BooleanExpression을 반환, 로그인하지 않은 상태에서는 모든 게시물이 북마크되지 않았다고 판단하므로 항상 false를 반환함

3. 해결 결과

브라우저 캐싱, 네트워크 상태 등으로 매번 다른 결과가 나와서 정확하게 측정할 수 없지만 수정전 평균적으로
530ms 정도 나오던게

평균적으로 400ms 정도로 나온다

4. 보완할점

  1. 야매로 개발자 도구가 아닌 제대로 된 성능 테스트 툴을 적용해야겠다...
  2. null을 저렇게 남발하면 안될거같다 null이 아닌 특정 값을 넣어서 repository 계층에 전달해야겠다

0개의 댓글