일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- bootcamp
- DynamoDB
- 자격증
- goorm
- aws
- serverless
- 백엔드
- Redis
- CICD
- jpa
- rds
- codebuild
- goorm x kakao
- Spring Boot
- backenddeveloper
- 스터디
- MSA
- CodeCommit
- sqs
- 오블완
- Docker
- 개발자
- ec2
- codedeploy
- QueryDSL
- 티스토리챌린지
- s3
- spring
- orm
- mapping
- Today
- Total
gony-dev 님의 블로그
[Querydsl] 스프링 데이터 JPA가 제공하는 Querydsl 기능 본문
지난 시간에는 스프링 데이터 JPA와 Querydsl의 기능을 비교하고,
사용자 정의 리포지토리를 만들어 Querydsl을 상속받는 인터페이스를 구현보았다.
이번에는 마지막 강의 정리로 복잡한 실무 환경에는 알맞지 않지만 스프링 데이터에서 제공하는 기능들을 살펴보겠다.
인터페이스 지원 - QuerydslPredicateExecutor
QuerydslPredicateExecutor는 인터페이스는 스프링 데이터 JPA에 Querydsl을 사용하기 위해 제공되는 기능이다.
이를 사용하기 위해서는 사용하고 있는 인터페이스에 상속하면 된다!
MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom, QuerydslPredicateExecutor<Member> {
// select m from Member m where m.username = ?
List<Member> findByUsername(String username);
}
MemberRepositoryTest.java
(데이터는 BeforeEach를 통해 사전에 넣어둔 상태이다!)
@DisplayName("QuerydslPredicateExecutor를 사용하여 멤버를 조회한다.")
@Test
void querydslPredicateExecutorTest(){
Iterable<Member> result = memberRepository.findAll(member.age.between(10, 40)
.and(member.username.eq("member1")));
for (Member member : result) {
System.out.println("member = " + member);
}
}
- 위의 테스트를 보면 Querydsl에 대한 사용자 정의 리포지토리를 사용하지 않았음에도 Querydsl을 사용하여 멤버를 조회했다.
- 이렇게 QuerydslPredicateExecutor를 쓰면 Querydsl의 기능을 기본적으로 제공받을 수 있다.
- 또한 Pagable, Sort를 모두 지원하고 정상 동작하는 특징이 있다!
한계점
- 실무 환경에서는 QuerydslPredicateExecutor를 사용하는 것이 적합하지 않다.
그 이유는 조인이 불가능하기 때문이다! - 또한 클라이언트가 Querydsl에 의존해야만 하며, 서비스 클래스가 Querydsl이라는 구현 기술에 의존해야만 한다.
- 이렇게 한계가 명확하기 때문에 복잡한 쿼리를 사용하는 실무 환경에서는 적합하지 않다.
Querydsl Web 지원
Querydsl Web은 Querydsl을 활용하여 웹 어플리케이션에서 동적 쿼리를 쉽게 생성하고 처리할 수 있도록 지원하는 기술이다.
주요 특징은 다음과 같다.
- 클라이언트의 요청 파라미터를 자동으로 파싱하여 동적 쿼리를 생성한다.
- Spring Data JPA와 통합되어, Controller에서 간단히 Querydsl을 사용할 수 있다.
- 조건문을 작성할 필요 없이 요청 파라미터를 바로 Querydsl로 매핑할 수 있어서 코드가 간결하다.
- Predicate 객체를 자동으로 생성해준다.
앞서나온 QuerydslPredicateExecutor와 함께 사용되어 동적 쿼리를 실행할 수 있다!
하지만 다음과 같은 한계점들로 사용을 지양하고 있다.
한계점
- 단순한 조건만 가능하다.
- 조건을 커스텀하는 기능이 복잡하고 명시적이지 않다.
- 컨트롤러가 Querydsl에 의존하게 된다.
위와 같은 이유들로 인해 마찬가지로 복잡한 실무환경에서 사용하는 것에 적합하지 않다!
리포지토리 지원 - QuerydslRepositorySupport
QuerydslRepositorySupport는 추상화 클래스로 Querydsl 라이브러리를 쓰기 위해 Repository 구현체가 받으면 편리한 클래스이다.
구버전의 Querydsl으로 3버전일 때 만들어졌으며 select가 아닌 from절부터 시작한다.
사용하는 방법은 다음과 같다.
MemberRepositoryImpl.class
public class MemberRepositoryImpl extends QuerydslRepositorySupport implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
// public MemberRepositoryImpl(JPAQueryFactory queryFactory) {
// this.queryFactory = queryFactory;
// }
public MemberRepositoryImpl(EntityManager em) {
super(Member.class);
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public List<MemberTeamDto> search (MemberSearchCondition condition){
List<MemberTeamDto> result = from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
).select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName"))
).fetch();
return result;
}
특징
- EntityManager가 제공되기 때문에 별도로 주입할 필요 없다.
- from절로 시작한다.
- 아래와 같은 코드로 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환할 수 있다!(단 Sort는 오류 발생!!)
JPQLQuery<MemberTeamDto> query = getQuerydsl().applyPagination(pageable, jpaQuery);
하지만 버전이 업데이트된 만큼 사용하지 않는 이유가 존재한다.
단점
- Querydsl 3.x 버전을 대상으로 만들었기 때문에 Querydsl 4.x에 나온 JPAQueryFactory로 시작할 수가 없다!
즉 select로 시작할 수 없다는 것이다. - QueryFactory를 제공하지 않는다.
- 스프링 데이터 Sort기능이 정상적으로 작동하지 않는다.
- 메서드 체인이 끊겨버려 디자인이 자연스럽지 않다.
이런 이유들로 사용하는 것을 지양하고 있으며 4.x을 사용하는 것이 바람직하다!
Querydsl 지원 클래스 직접 만들기
위의 "QuerydslRepositorySupport"는 여러 가지 이유로 사용하는 것을 지양하였다.
그럼 한계들을 극복하는 직접 Querydsl 지원 클래스를 만들어보자!
Querydsl4RepositorySupport.class
@Repository
public abstract class Querydsl4RepositorySupport {
private final Class domainClass;
private Querydsl querydsl;
private EntityManager entityManager;
private JPAQueryFactory jpaQueryFactory;
protected 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.jpaQueryFactory = 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(jpaQueryFactory, "JPAQueryFactory must not be null");
}
protected JPAQueryFactory getJpaQueryFactory() { return jpaQueryFactory; }
protected Querydsl getQuerydsl() { return querydsl; }
protected EntityManager getEntityManager() { return entityManager; }
protected <T> JPAQuery<T> select(Expression<T> expr) {
return getJpaQueryFactory().select(expr);
}
protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) {
return getJpaQueryFactory().selectFrom(from);
}
protected <T> Page<T> applyPagination(Pageable pageable, Function<JPAQueryFactory, JPAQuery> contentQuery) {
JPAQuery jpaQuery = contentQuery.apply(getJpaQueryFactory());
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 jpaQuery = contentQuery.apply(getJpaQueryFactory());
List<T> content = getQuerydsl().applyPagination(pageable, jpaQuery).fetch();
JPAQuery countResult = countQuery.apply(getJpaQueryFactory());
return PageableExecutionUtils.getPage(content, pageable, countResult::fetchCount);
}
}
직접 Querydsl 지원 클래스를 만들어 사용하면 장점은 다음과 같다.
- 스프링 데이터가 제공하는 페이징을 편리하게 변환할 수 있다.
- 페이징과 카운트 쿼리를 분리할 수 있다.
- 스프링 데이터 Sort를 지원한다!
- 'select()'절로 시작이 가능하다.
- "EntityManager", "QueryFactory"를 지원한다!
구현하면서 확인해보자!
MemberTestRepository.class
@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, query ->
query.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
.select(member.id)
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
);
}
private BooleanExpression usernameEq(String username) {
return hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return 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.eq(ageLoe) : null;
}
}
- 테스트 리포지토리를 통해 우리는 Querydsl 4.X과 3.X을 잘 조합하여 사용할 수 있었다.
결론
Querydsl4RepositorySupport는 Spring Data JPA와 Querydsl을 통합하여 코드의 생산성과 유지보수성을 높힌다.
단순히 Querydsl만 사용하였을 때 보다 편리한 기능을 제공해 주는데 이는 다음과 같다.
- getQuerydsl().applyPagination() 메서드를 제공하여 페이징 로직을 단순화할 수 있다.
- 반복되는 설정 코드를 캡슐화하기에, 필요한 쿼리 로직에만 집중할 수 있다.
- 단일 책임 원칙을 준수하며, 새로운 검색 조건이 추가되어도 쉽게 확장할 수 있다!
하지만 코드 중복이 많지 않거나 동적 쿼리 및 페이징 처리에 익숙하다면 굳이 사용하지는 않아도 될 것 같다.
'Querydsl' 카테고리의 다른 글
[Querydsl] 스프링 데이터 JPA와 Querydsl (0) | 2024.12.21 |
---|---|
[Querydsl] 순수 JPA와 Querydsl (1) | 2024.12.09 |
[Querydsl] 중급 문법 (2) | 2024.12.03 |
[Querydsl] 기본 문법 (0) | 2024.11.25 |
[Querydsl] 초기 설정하기(Spring boot 3.x.x 이상) (1) | 2024.11.17 |