ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 실무 활용 - 스프링 데이터 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
Designed by Tistory.