5 분 소요

순수 JPA와 Querydsl

시작하기 전에 JPAQueryFactory를 사용할 수 있도록 설정해줘야 한다.
main()이 정의돼있는 QuerydslApplication 클래스에서 다음 내용을 적어주자.
JPAQueryFactory을 스프링 빈으로 등록해주는 작업이며 EntityManager을 파라미터로 받아 적용한다.

@SpringBootApplication
public class QuerydslApplication {

	public static void main(String[] args) {...}

	@Bean
	JPAQueryFactory jpaQueryFactory(EntityManager em) {
		return new JPAQueryFactory(em);
	}
}


repository 이름은 MemberJpaRepository로 정의하고, 위에서 적었던 JPAQueryFactory를 사용하기 위해
다음과 같이 설정해주자.

@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {

    private final EntityManager em;
    private final JPAQueryFactory queryFactory;
    ...
}
  • em, queryFactory에 대한 생성자를 별도로 생성해줘도 상관없음
  • 해당 코드 작성 생략을 위해 @RequiredArgsConstructor 적용

순수 JPA 코드는 간단간단하다. 필요할 땐 jpql 써서 처리해주면 된다.

public List<Member> findAll() {
    return em.createQuery("select m from Member m", Member.class)
            .getResultList();
}

public List<Member> findByUsername(String username) {
    return em.createQuery("select m from Member m where m.username = :username", Member.class)
            .setParameter("username", username)
            .getResultList();
}


하지만 위 코드는 실행해보기 전까지 에러를 찾기 어렵다는 단점이 있다.
그래서 querydsl을 사용하면 다음처럼 바꿀 수 있고 런타임에 에러를 잡을 수 있다는 장점이 있다.

public List<Member> findAll_Querydsl() {
    return queryFactory
            .selectFrom(member)
            .fetch();
}

public List<Member> findByUsername_Querydsl(String username) {
    return queryFactory
            .selectFrom(member)
            .where(member.username.eq(username))
            .fetch();
}


Builder 적용

builder를 적용해 좀 더 복잡한 조건들을 처리해보자.
Member의 이름, 나이 그리고 Team의 이름에 대해 조건을 걸어주기 위해 MemberSearchCondition를 정의하자.

@Data
public class MemberSearchCondition {

    private String username;
    private String teamName;
    private Integer ageGoe;
    private Integer ageLoe;
}


그리고 엔티티가 아닌 DTO를 리턴할 용도로 MemberTeamDto을 정의해주자.

@Data
public class MemberTeamDto {

    private Long memberId;
    private String username;
    private int age;
    private Long teamId;
    private String teamName;

    @QueryProjection
    public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) {
        this.memberId = memberId;
        this.username = username;
        this.age = age;
        this.teamId = teamId;
        this.teamName = teamName;
    }
}
  • @QueryProjection를 적용해 repository에서 DTO를 쉽게 생성할 수 있음

BooleanBuilder 타입인 builder를 생성하주고 조건들을 적용해주자.
다음 코드는 이름이 같은지, 나이가 범위안에 들어오는지 체크해주는 로직이다.

BooleanBuilder builder = new BooleanBuilder();
if(StringUtils.hasText(condition.getUsername())) {
    builder.and(member.username.eq(condition.getUsername()));
}
if(StringUtils.hasText(condition.getTeamName())) {
    builder.and(team.name.eq(condition.getTeamName()));
}
if(condition.getAgeGoe() != null) {
    builder.and(member.age.goe(condition.getAgeGoe()));
}
if(condition.getAgeLoe() != null) {
    builder.and(member.age.loe(condition.getAgeLoe()));
}
  • StringUtils.hasText()는 null 체크를 하기위해 사용

그리고 위에서 만든 builder를 where절에 적용해주면 끝난다.

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(builder)
        .fetch();
  • QMemberTeamDto 생성자로 DTO를 쉽게 생성할 수 있는건 @QueryProjection를 적용했기 때문
  • QMemberTeamDto를 사용하기 위해 compileQuerydsl 반드시 체크

where절 파라미터 사용

builder 사용 시 단점이 있다면 builder 코드가 복잡하다는 점이 있다. 내용이 길어지면 가독성도 떨어지게 된다.
다음과 같이 조건들을 쪼개서 함수를 개별로 선언하고 where절에 적용하면 가독성도 올라가고 코드도 간단해진다.
여기서 선언한 함수는 다른 로직에서 재활용할 수 있다는 장점도 있다!

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();
}

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;
}
  • Predicate보다는 BooleanExpression을 리턴 타입으로 적용할 것

controller 개발

위에서 만들었던 repository 코드를 그대로 적용만 해주면 된다.

@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberJpaRepository memberJpaRepository;

    @GetMapping("/v1/members")
    public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) {
        return memberJpaRepository.search(condition);
    }
}


스프링 데이터 JPA와 Querydsl

스프링 데이터 JPA + Querydsl repository

스프링 데이터 JPA를 적용하기 위해 MemberRepository를 생성하자.
위에서 save(), findAll() 등은 기본으로 제공하지만 username으로 조회하는 함수는 없으므로 만들어주자.

public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByUsername(String username);
}


위에서 querydsl를 적용해 만들었던 search()MemberRepository가 interface이므로 그대로 쓸 수 없다.
때문에 사용자 정의 리포지토리가 필요하다. 일단 사용자 정의 인터페이스부터 작성하자.

public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
}
  • 이름은 마음대로 설정해도 상관없음
  • 위에서 querydsl로 구현했던 search()를 사용하기 위해 해당 인터페이스에 정의

그리고 MemberRepositoryImpl를 생성해 방금 만든 MemberRepositoryCustom interface를 구현해주자.
이 때 JPAQueryFactory를 사용해야 하므로 선언해준 뒤 EntityManager로 주입해주자.
위에서 구현했던 search(), usernameEq(), teamNameEq(), ageGoe(), ageLoe()를 그대로 가져오자.

public class MemberRepositoryImpl implements MemberRepositoryCustom {

    private final JPAQueryFactory queryFactory;

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

    @Override
    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();
    }

    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;
    }
}
  • search()MemberRepositoryCustom에 정의된 함수이므로 오버라이딩 필수
  • 이름은 반드시 상속할 repository 이름 뒤에 Impl이 반드시 붙어야 함
    • MemberRepository를 상속할거니까 MemberRepositoryImpl로 설정

MemberRepository에서 search()를 사용할 수 있도록 MemberRepositoryCustom를 상속받자!

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
    List<Member> findByUsername(String username);
}


Querydsl 페이징 설정

방법은 2가지가 있고 카운트 쿼리를 따로 설정해주는지 여부에 따라 달라진다.
별도로 설정해주지 않는 함수를 searchPageSimple(), 별도로 설정해주는 함수를 searchPageComplex()라 하자.
MemberRepositoryCustom에 위 2개 함수를 정의해주자.

public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
    Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
    Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}


MemberRepositoryImplsearchPageSimple()를 정의해주자.
search()에서 쓰는 쿼리는 같지만 페이징 처리를 위해 코드를 일부 수정했다.

@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
    QueryResults<MemberTeamDto> results = 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())
            )
            .fetchResults();

    List<MemberTeamDto> content = results.getResults();
    long total = results.getTotal();

    return new PageImpl<>(content, pageable, total);
}
  • List가 아닌 Page를 리턴함에 주의
  • fetch()가 아닌 fetchResults()를 적용
    • 내용과 전체 카운트를 한번에 조회 가능 = 쿼리 2번 호출

카운트 쿼리를 별도로 설정해주는 searchPageComplex()를 정의해주자.
searchPageSimple()와 거의 유사하고, 카운트 관련 코드를 수정했다.

@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())
            )
            .fetch();

    //직접 total count 쿼리 날림
    long total = queryFactory
            .selectFrom(member)
            .leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())
            )
            .fetchCount();

    return new PageImpl<>(content, pageable, total);
}
  • fetchResults()가 아닌 fetch()를 사용함에 주의
  • total 뒤 코드를 보면 카운트 쿼리를 직접 querydsl로 만들어서 날리고 있음
  • PageImplPage의 구현체

CountQuery 최적화

searchPageComplex()에서 카운트 쿼리가 필요없는 일부 상황에 대해 처리하는 스프링 데이터 라이브러리를
활용한다면 최적화가 가능하다! total부터 내용을 주석처리하고 다음과 같이 처리해주면 된다.

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

return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
  • 카운트 쿼리가 필요없는 상황
    • 시작 페이지에서 컨텐츠 사이즈 < 페이지 사이즈
    • 마지막 페이지 일 때, 컨텐츠 사이즈 < 페이지 사이즈
  • PageableExecutionUtils.getPage()를 사용하면 해당 상황들을 알아서 처리해줘서 최적화 가능

controller 개발

위에서 만든 MemberRepository를 추가해주고 구현했던 함수를 그대로 활용해주면 된다.
페이징 처리를 위해 Pageable를 파라미터에 넣어줘야 함에 주의!

@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberJpaRepository memberJpaRepository;
    private final MemberRepository memberRepository; //추가

    //v1 API 생략

    @GetMapping("/v2/members")
    public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable) {
        return memberRepository.searchPageSimple(condition, pageable);
    }

    @GetMapping("/v3/members")
    public Page<MemberTeamDto> searchMemberV3(MemberSearchCondition condition, Pageable pageable) {
        return memberRepository.searchPageComplex(condition, pageable);
    }
}

카테고리:

업데이트:

댓글남기기