Querydsl 활용

이상훈·2022년 11월 4일
0

Jpa

목록 보기
14/16

김영한님의 인프런 강의 '실전! Querydsl'을 참고했습니다.

스프링 데이터 JPA를 활용하면 리포지토리 계층에서 순수 JPA를 사용했을 때 대비 코드량을 상당히 줄일 수 있다.

public interface MemberRepository extends JpaRepository<Member, Long> {

	List<Member> findByUsername(String username); //메서드 이름으로 쿼리 생성 방식
}

하지만 Querydsl의 동적 쿼리 생성 기능을 사용하려면 사용자 정의 리포지토리가 필요하다.


사용자 정의 리포지토리

✔️ 사용자 정의 리포지토리 구성

1. 사용자 정의 인터페이스 작성

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

	Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);

}

2. 사용자 정의 인터페이스 구현

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,
 				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();
	}
 
    @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);
	}
    
    @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();
		return new PageImpl<>(content, pageable, total);
	}
    
    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);
	}
 
}

  • search 메서드 :
    • 회원명, 팀명, 나이 조건에 따른 동적 쿼리를 생성한다.
  • searchPageSimple 메서드 :
    • 전체 카운트를 한번에 조회하는 단순한 방법이다.
    • .fetchResults() : 내용과 전체 카운트를 한번에 조회한다.
  • searchPageComplex 메서드 :
    • 데이터 내용과 전체 카운트를 별도로 조회하는 방법으로 성능 최적화와 관련이 있다.
    • .fetch를 사용하고 total count형 쿼리를 따로 만든다.

참고 : JPAQueryFactory 스프링 빈 등록
JPAQueryFactory를 스프링 빈으로 등록해서 주입받아 사용해도 된다.

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

참고 : 동적 쿼리
동적 쿼리를 구현하는 방법으로 다음 두가지가 있다.

  • BooleanBuilder
  • Where 다중 파라미터 사용

여기서는 Where 다중 파라미터 방식을 사용했는데 이 방식이 가독성도 좋고 이점이 많다.


참고 : CountQuery 최적화

// return new PageImpl<>(content, pageable, total);
 return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);

PageableExecutionUtils.getPage()는 아래와 같은 경우 Count 쿼리를 생략해서 처리한다. 최적화에 도움이 된다.

  • 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
  • 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)

PageableExecutionUtils.getPage()를 사용하자.


참고 : 정렬

정렬(Sort)은 조건이 조금만 복잡해져도 Pageable의 Sort 기능을 사용하기 어렵다. 루트 엔티티 범위를 넘어가는 동적 정렬 기능이 필요하면 스프링 데이터 페이징이 제공하는 Sort를 사용하기보다는 파라미터를 받아서 직접 처리하는 것을 권장한다.


3. 사용자 정의 인터페이스 상속

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {

	List<Member> findByUsername(String username);
}

조회 API 컨트롤러 개발

프로파일 설정

먼저 테스트를 실행할 때와 tomcat 서버를 띄울 때를 서로 다른 케이스를 가지고 할 때 프로파일 설정이 필요하다.

main
src/main/resources/application.yml

spring:
	profiles:
		active: local

test
src/test/resources/application.yml

spring:
	profiles:
		active: test

샘플 데이터 추가

아래와 같이 샘플 데이터를 추가해 주면 Tomcat을 통해 서버를 실행할 때만 이 데이터들을 활용할 수 있다.

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

	private final InitMemberService initMemberService;
	
    @PostConstruct
	public void init() {
		initMemberService.init();
	}

	@Component
	static class InitMemberService {

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

조회 컨트롤러

ex ) http://localhost:8080/v2/members?size=5&page=2

@RestController
@RequiredArgsConstructor
public class MemberController {

	private final MemberJpaRepository memberJpaRepository;
	private final MemberRepository memberRepository;

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

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

기타

스프링 데이터 JPA는 다음과 같은 Querydsl 기능을 제공한다.

1. 인터페이스 지원 : QuerydslPredicateExecutor 인터페이스
2. Querydsl Web 지원 : 공식 URL
3. 리포지토리 지원 : QuerydslRepositorySupport

하지만 위 기능들은 실무에서 한계가 명확해 사용하지 않는다.

4. Querydsl 지원 클래스 직접 만들기 : QuerydslRepositorySupport의 한계를 극복하기 위해 사용한다. Advanced 과정, 나중에 공부하자.

profile
Problem Solving과 기술적 의사결정을 중요시합니다.

1개의 댓글

comment-user-thumbnail
2022년 11월 6일

JPA + Querydsl + DTO를 섞어 쓰기 시작하면 지옥문이 열리기 시작한다. JPA 순수기능(영속성 컨텍스트, Entity)을 코드상에서 최대한 배제해야 한다. Querydsl + DTO만 쓰는 것처럼 소스 코드를 작성해야 한다. 소스상에서 JPA repository interface/Querydsl 또는 Entity class/DTO class가 섞여 있으면 일관성 있는 코드 작성이 어려워지고 쉽게 풀지 못할 난관을 만나게 된다. JPA는 Entity class만 잘 작성하는 것으로도 큰 어려움을 넘어서는 건 아닐까 생각합니다. 브릿지 테이블을 생성하는 시점부터 JPA로 실행되는 부분을 소스상에서 완전히 배제했다.

답글 달기