[Querydsl] Spring Data JPA + Querydsl

Dev_Sanizzang·2023년 5월 23일
0

Querydsl

목록 보기
3/3

순수 JPA와 Querydsl

순수 JPA와 Querydsl같은 경우는 기본 JPA와 전에 했던 Querydsl을 이해했다면 쉽게 이해할 수 있기 때문에 깃허브 코드를 참고하면 될 것 같다.

💡 JPAQueryFactory는 스프링 빈으로 등록 후 사용할 수 있다.

@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
	return new JPAQueryFactory(em);
}
  • 테스트 코드짤 때 좀 귀찮아질 수 있다는 단점이 있다.

프로파일 설정

편리한 데이터 확인을 위해 샘플 데이터를 추가할 수 있다.
샘플 데이터 추가가 테스트 케이스 실행에 영향을 주지 않게 하려면 아래와 같이 프로파일을 설정해야한다.

src/main/resourcs/application.yml

spring:
  profiles:
    active: local

src/test/resourcs/application.yml

spring:
  profiles:
    active: test

InitMember.java

@Profile("local")
@Component
@RequiredArgsConstructor
public class InitMember {

    private final InitMemberService initMemberService;

    @PostConstruct
    public void init() {
    	// PostConstruct에 initMemberService 코드를 바로 넣을 수 없다.
        initMemberService.init();
    }

    @Component
    static class InitMemberService {
        @PersistenceContext
        private EntityManager em;

        @Transactional
        public void init() {
            Team teamA = new Team("teamA");
            Team teamB = new Team("teamB");
            em.persist(teamA);
            em.persist(teamB);

            for (int i = 0; i < 100; i++) {
                Team selectedTeam = i % 2 == 0 ? teamA : teamB;
                em.persist(new Member("member" + i, i, selectedTeam));
            }
        }
    }
}

이렇게 프로파일을 분리하면 main 소스코드와 테스트 소스 코드 실행시 프로파일을 분리할 수 있다.

Spring Data JPA + Querydsl

기본적인 간단한 정적쿼리는 Spring Data JPA의 구현체를 통해 사용하고 복잡한 쿼리들은 Querydsl이 필요하다.
Querydsl을 쓰려면 결국 구현코드를 만들어야 하는데, Spring Data JPA같은 경우는 interface로 동작을한다. 어떻게 적용시킬까?

사용자 정의 리포지토리 사용

  1. 사용자 정의 인터페이스 작성
public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
    Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
    Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
  1. 사용자 정의 인터페이스 구현
public class MemberRepositoryImpl implements MemberRepositoryCustom{

    private final JPAQueryFactory queryFactory;

    public MemberRepositoryImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }

    public List<MemberTeamDto> search(MemberSearchCondition condition) {
        return queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .fetch();
    }

    @Override
    public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        int total = queryFactory
                .select(member)
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .fetch().size();

        return new PageImpl<>(content, pageable, total);
    }

    @Override
    public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        JPAQuery<Member> countQuery = queryFactory
                .select(member)
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                );

        return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetch().size());

//        return new PageImpl<>(content, pageable, total);
    }

    private BooleanExpression usernameEq(String username) {
        return StringUtils.hasText(username) ? member.username.eq(username) : null;
    }

    private BooleanExpression teamNameEq(String teamName) {
        return StringUtils.hasText(teamName) ? team.name.eq(teamName): null;
    }

    private BooleanExpression ageGoe(Integer ageGoe) {
        return ageGoe != null ? member.age.goe(ageGoe) : null;
    }

    private BooleanExpression ageLoe(Integer ageLoe) {
        return ageLoe != null ? member.age.loe(ageLoe) : null;
    }

}

💡 Impl에는 조건이 있다. Spring Data JPA을 사용하기 위해 상속받은 인터페이스 명 + Impl로 작성해줘야 한다.

  1. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
    List<Member> findByUsername(String username);
}

💡 interface 같은 경우는 여러개를 상속 가능하다.

고민할 점(설계)

너무 조회가 복잡하면 아키텍쳐적인 고민이 필요할 수 있다.
특정한 기능에 맞춰진 조회기능이면 사용자 정의 인터페이스를 생성하고 Spring Data JPA에 상속할 필요 없이 별도의 Repository 구현체를 만들어주고 해당 Repository를 사용하는 것도 고려할 수 있다.

  • 공용성이 없고, 특정 API에 종속되어있다면 수정 라이프 사이클 자체가 화면에 맞춰져서 기능이 변경이 된다.
    -> 별도의 조회용 Repository를 만들자

💡 모든 걸 Custom에 넣을 필요는 없다.

Spring Data JPA 페이징 활용

💡 fetchResults(), fetchCount() Deprecated
해당 블로그를 보면 count 쿼리가 모든 dialect에서 또는 다중 그룹 쿼리에서 완벽하게 지원되지 않기 때문에 deprecated 되었다고 한다. 고로 fetchCount() 대신 fetch().size()로 동일한 결과를 얻을 수 있다고 설명한다.

@Override
    public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        int total = queryFactory
                .select(member)
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .fetch().size();

        return new PageImpl<>(content, pageable, total);
    }

CountQuery 최적화

count 쿼리가 생략 가능한 경우 생략해서 처리한다.

  • 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
  • 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)
@Override
    public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        JPAQuery<Member> countQuery = queryFactory
                .select(member)
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                );

        return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetch().size());

//        return new PageImpl<>(content, pageable, total);
    }
  • Spring Data JPA가 제공하는 PageableExecutionUtils.getPage() 사용

🚪 마무리

이것으로 Querydsl의 기본적인 학습을 마쳤다. 이를 바탕으로 프로젝트에 적용시켜보도록 하자!

profile
기록을 통해 성장합니다.

0개의 댓글