-
실무 활용 - 스프링 데이터 JPA와 Querydsl이커머스 devops/국비교육 2025. 11. 19. 21:47
스프링 데이터 JPA 리포지토리로 변경
// 스프링 데이터 JPA - MemberRepository 생성 public interface MemberRepository extends JpaRepository<Member, Long> { List<Member> findByUsername(String username); }사용자 정의 리포지토리 사용법

- 사용자 정의 인터페이스 작성
- 사용자 정의 인터페이스 구현
- 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
1. 사용자 정의 인터페이스 작성
public interface MemberRepositoryCustom { List<MemberTeamDto> search(MemberSearchCondition condition); }2. 사용자 정의 인터페이스 구현
public class MemberRepositoryImpl implements MemberRepositoryCustom { private final JPAQueryFactory queryFactory; public MemberRepositoryImpl(EntityManager em) { this.queryFactory = new JPAQueryFactory(em); } @Override //회원명, 팀명, 나이(ageGoe, ageLoe) public List<MemberTeamDto> search(MemberSearchCondition condition) { return queryFactory .select(new QMemberTeamDto( member.id, member.username, member.age, team.id, team.name)) .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 isEmpty(username) ? null : member.username.eq(username); } private BooleanExpression teamNameEq(String teamName) { return isEmpty(teamName) ? null : team.name.eq(teamName); } private BooleanExpression ageGoe(Integer ageGoe) { return ageGoe == null ? null : member.age.goe(ageGoe); } private BooleanExpression ageLoe(Integer ageLoe) { return ageLoe == null ? null : member.age.loe(ageLoe); } }3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
public interface MemberRepository extends JpaRepository<Member, Long>,MemberRepositoryCustom { List<Member> findByUsername(String username); }4. 커스텀 리포지토리 동작 테스트 추가
@Test public void searchTest() { Team teamA = new Team("teamA"); Team teamB = new Team("teamB"); em.persist(teamA); em.persist(teamB); Member member1 = new Member("member1", 10, teamA); Member member2 = new Member("member2", 20, teamA); Member member3 = new Member("member3", 30, teamB); Member member4 = new Member("member4", 40, teamB); em.persist(member1); em.persist(member2); em.persist(member3); em.persist(member4); MemberSearchCondition condition = new MemberSearchCondition(); condition.setAgeGoe(35); condition.setAgeLoe(40); condition.setTeamName("teamB"); List<MemberTeamDto> result = memberRepository.search(condition); assertThat(result).extracting("username").containsExactly("member4"); }스프링 데이터 페이징 활용 - 1. Querydsl 페이징 연동
사용자 정의 인터페이스에 페이징 2가지 추가
public interface MemberRepositoryCustom { List<MemberTeamDto> search(MemberSearchCondition condition); // 전체 카운트를 한번에 조회하는 단순한 방법 Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable); // 데이터 내용과 전체 카운트를 별도로 조회하는 방법 Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable); }1-1. 전체 카운트를 한번에 조회하는 단순한 방법 - searchPageSimple(), fetchResults() 사용
/** * 단순한 페이징, fetchResults() 사용 */ @Override public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) { QueryResults<MemberTeamDto> results = queryFactory .select(new QMemberTeamDto( member.id, member.username, member.age, team.id, team.name)) .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()) .fetchResults(); List<MemberTeamDto> content = results.getResults(); long total = results.getTotal(); return new PageImpl<>(content, pageable, total); }- Querydsl이 제공하는 fetchResult()를 사용하면 내용과 전체 카운트를 한 번에 조회할 수 있다 (쿼리 2번 호출)
- fetchResult()는 카운트 쿼리 실행시 필요없는 order by는 제거한다
1-2. 데이터 내용과 전체 카운트를 별도로 조회하는 방법 - searchPageComplex() 사용
/** * 복잡한 페이징 * 데이터 조회 쿼리와, 전체 카운트 쿼리를 분리 */ @Override public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) { List<MemberTeamDto> content = queryFactory .select(new QMemberTeamDto( member.id, member.username, member.age, team.id, team.name)) .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(); long total = queryFactory .select(member) .from(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe())) .fetchCount(); JPAQuery<Long> countQuery = queryFactory .select(member.count()) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe())); // return new PageImpl<>(content, pageable, total); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); }- 전체 카운트를 조회 하는 방법을 최적화 할 수 있으면 이렇게 분리한다
- 전체 카운트할 때 조인 쿼리를 줄일 수 있다면 상당한 효과가 있다
스프링 데이터 페이징 활용2 - CountQuery 최적화
PageableExecutionUtils.getPage()로 최적화
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 new PageImpl<>(content, pageable, total); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);- 스프링 데이터 라이브러리가 제공
- count 쿼리가 생략 가능한 경우 생략해서 처리
- 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
- 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구한다)
스프링 데이터 정렬(Sort)
스프링 데이터 Sort를 Querydsl의 OrderSpecifier로 변환
JPAQuery<Member> query = queryFactory .selectFrom(member); for (Sort.Order o : pageable.getSort()) { PathBuilder pathBuilder = new PathBuilder(member.getType(), member.getMetadata()); query.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC, pathBuilder.get(o.getProperty()))); } List<Member> result = query.fetch();- 정렬은 조건이 조금만 복잡해져도 Pageable의 sort를 사용하기 어렵다
- 스프링 데이터 페이징이 제공하는 Sort를 사용하기 보다는 파라미터를 받아서 직접 처리하는 것을 권장
Querydsl 지원 클래스 직접 만들기
- 스프링 데이터가 제공하는 페이징을 편리하게 변환
- 페이징과 카운트 쿼리 분리 가능
- 스프링 데이터 Sort 지원
- select(), selectFrom() 으로 시작 가능
- EntityManager, QueryFactory 제공
Querydsl4RepositorySupport 작성
/** * * Querydsl 4.x 버전에 맞춘 Querydsl 지원 라이브러리 * @author Younghan Kim * @see org.springframework.data.jpa.repository.support.QuerydslRepositorySupport */ @Repository public abstract class Querydsl4RepositorySupport { private final Class domainClass; private Querydsl querydsl; private EntityManager entityManager; private JPAQueryFactory queryFactory; public Querydsl4RepositorySupport(Class<?> domainClass) { Assert.notNull(domainClass, "Domain class must not be null!"); this.domainClass = domainClass; } @Autowired public void setEntityManager(EntityManager entityManager) { Assert.notNull(entityManager, "EntityManager must not be null!"); JpaEntityInformation entityInformation = JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager); SimpleEntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE; EntityPath path = resolver.createPath(entityInformation.getJavaType()); this.entityManager = entityManager; this.querydsl = new Querydsl(entityManager, new PathBuilder<>(path.getType(), path.getMetadata())); this.queryFactory = new JPAQueryFactory(entityManager); } @PostConstruct public void validate() { Assert.notNull(entityManager, "EntityManager must not be null!"); Assert.notNull(querydsl, "Querydsl must not be null!"); Assert.notNull(queryFactory, "QueryFactory must not be null!"); } protected JPAQueryFactory getQueryFactory() { return queryFactory; } protected Querydsl getQuerydsl() { return querydsl; } protected EntityManager getEntityManager() { return entityManager; } protected <T> JPAQuery<T> select(Expression<T> expr) { return getQueryFactory().select(expr); } protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) { return getQueryFactory().selectFrom(from); } protected <T> Page<T> applyPagination(Pageable pageable, Function<JPAQueryFactory, JPAQuery> contentQuery) { JPAQuery jpaQuery = contentQuery.apply(getQueryFactory()); List<T> content = getQuerydsl().applyPagination(pageable, jpaQuery).fetch(); return PageableExecutionUtils.getPage(content, pageable, jpaQuery::fetchCount); } protected <T> Page<T> applyPagination(Pageable pageable, Function<JPAQueryFactory, JPAQuery> contentQuery, Function<JPAQueryFactory, JPAQuery> countQuery) { JPAQuery jpaContentQuery = contentQuery.apply(getQueryFactory()); List<T> content = getQuerydsl().applyPagination(pageable, jpaContentQuery).fetch(); JPAQuery countResult = countQuery.apply(getQueryFactory()); return PageableExecutionUtils.getPage(content, pageable, countResult::fetchCount); } }Querydsl4RepositorySupport 사용 코드
@Repository public class MemberTestRepository extends Querydsl4RepositorySupport { public MemberTestRepository() { super(Member.class); } public List<Member> basicSelect() { return select(member) .from(member) .fetch(); } public List<Member> basicSelectFrom() { return selectFrom(member) .fetch(); } public Page<Member> searchPageByApplyPage(MemberSearchCondition condition, Pageable pageable) { JPAQuery<Member> query = selectFrom(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe())); List<Member> content = getQuerydsl().applyPagination(pageable, query).fetch(); return PageableExecutionUtils.getPage(content, pageable, query::fetchCount); } public Page<Member> applyPagination(MemberSearchCondition condition, Pageable pageable) { return applyPagination(pageable, contentQuery -> contentQuery .selectFrom(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()))); } public Page<Member> applyPagination2(MemberSearchCondition condition, Pageable pageable) { return applyPagination(pageable, contentQuery -> contentQuery .selectFrom(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe())), countQuery -> countQuery .selectFrom(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()))); } private BooleanExpression usernameEq(String username) { return isEmpty(username) ? null : member.username.eq(username); } private BooleanExpression teamNameEq(String teamName) { return isEmpty(teamName) ? null : team.name.eq(teamName); } private BooleanExpression ageGoe(Integer ageGoe) { return ageGoe == null ? null : member.age.goe(ageGoe); } private BooleanExpression ageLoe(Integer ageLoe) { return ageLoe == null ? null : member.age.loe(ageLoe); } }728x90'이커머스 devops > 국비교육' 카테고리의 다른 글
P6Spy 적용하기 (0) 2025.11.22 Resolved [org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: (0) 2025.11.22 동적 쿼리와 성능 최적화 조회 (0) 2025.11.19 QueryDSL 중급 문법 (0) 2025.11.19 QueryDSL 기본 문법 (0) 2025.11.19