Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- Redis
- QueryDSL
- goorm x kakao
- serverless
- 자격증
- backenddeveloper
- jpa
- sqs
- mapping
- 개발자
- DynamoDB
- MSA
- orm
- spring
- codebuild
- Spring Boot
- Docker
- 오블완
- codedeploy
- bootcamp
- 스터디
- s3
- data
- CodeCommit
- rds
- 티스토리챌린지
- aws
- nosql
- goorm
- CICD
Archives
- Today
- Total
gony-dev 님의 블로그
[Querydsl] 스프링 데이터 JPA와 Querydsl 본문

지난 시간에는 Repository를 통해 순수 JPA를 만들고 이를 Querydsl과 비교하는 시간을 가졌다.
이번에는 Spring Data JPA를 이용한 방법과 Querydsl과의 차이를 알아보는 시간을 가져보도록 하겠다.
스프링 데이터 JPA 리포지토리로 변경
이제 우리는 JpaRepository를 상속받은 인터페이스를 통해 제공되는 메서드로 간단한 쿼리를 조회할 수 있다!
MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long> {
// select m from Member m where m.username = ?
List<Member> findByUsername(String username);
}
- 문득 코드를 보면 이런 생각을 할 수도 있다.
"어? 왜 @Repository를 명시하지 않지?" - 결론부터 말하면 어노테이션을 사용할 필요가 없다!
그 이유는 JpaRepository를 상속받으면 repository 인터페이스를 빈으로 등록해주기 때문이다. - 이는 코드를 뜯어보면서 알아볼 수 있는데 간단히 설명하면 @EnableJpaRepositories에서 빈을 등록해준다고 이해하면 될 것 같다.
MemberRepositoryTest
@Test
void basicTest() {
Member member = new Member("member1", 10);
memberRepository.save(member);
Member findMember = memberRepository.findById(member.getId()).get();
assertThat(findMember).isEqualTo(member);
List<Member> result1 = memberRepository.findAll();
assertThat(result1).containsExactly(member);
List<Member> result2 = memberRepository.findByUsername("member1");
assertThat(result2).containsExactly(member);
}
정상적으로 조회가 되는 것을 확인할 수 있다!

사용자 정의 리포지토리
Querydsl은 사전에 제공되지 않기 때문에 구현 코드를 만들어야 한다.
그러므로 Querydsl을 사용하고 싶다면 커스텀 리포지토리를 만들어 사용해보자!
커스텀 리포지토리를 사용하는 방법은 다음과 같다.
- 사용자 정의 인터페이스 작성
- 사용자 정의 인터페이스 구현
- 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
// select m from Member m where m.username = ?
List<Member> findByUsername(String username);
}
- 기존에 만들어두었던 MemberRepository에 MemberRepositoryCustom을 상속받도록 한다.
MemberRepositoryCustom
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
MemberRepositoryImpl
public class MemberRepositoryImpl 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){
return queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName"))
)
.from(member)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
.leftJoin(member.team, team)
.fetch();
}
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;
}
}
- 다음과 같이 MemberRepositoryCustom의 구현체인 search를 만들어 Where절 파라미터를 사용한 쿼리문을 작성해 보았다.
- 위의 쿼리는 팀을 조인하여 각 조건에 걸맞는 결과를 QMemberTeamDto에 변환하여 넣어주어 반환을 하게 된다.
MemberRepositoryTest
@DisplayName("BooleanExpression을 통해 조건에 맞는 결과를 반환한다.")
@Test
void searchTest(){
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,2 - Querydsl 페이징 연동과 CountQuery 최적화
스프링 데이터의 Page, Pageable은 JPA를 통해 반환해보았다.
이번에는 Querydsl을 통해 작성을 해보자.
구현할 메서드는 전체 카운트를 한 번에 조회하는 메서드와 데이터 내용과 전체 카운트를 별도로 조회하는 메서드를 구현해보겠다.
MemberRepositoryCustom
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
MemberRepositoryImpl
@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())
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults(); // fetchResults()는 더이상 사용하지 않기에 실무에선 fetch()를 사용하자!
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.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())
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch(); // fetchResults()는 더이상 사용하지 않기에 실무에선 fetch()를 사용하자!
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 PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount); // Page 최적화
}
private long getTotal(MemberSearchCondition condition) {
return queryFactory
.select(member)
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
.fetchCount();
}
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;
}
- 다음과 같이 searchPageSimple과 searchPageComplex를 구현해보았다.
각 메서드를 알아보도록 하자. - searchPageSimple() : 앞서 나온 search 메서드와 비슷하나, Paging 기술을 추가하여 offset과 limit를 이용하여 사용자가 원하는 만큼의 데이터를 조회하고 해당 결과를 getTotal을 사용하여 각 조건에 맞는 데이터를 조회하고 바로 total로 반환하여 하나의 Page<MemberTeamDto>에 모든 것을 담아내었다.
@BeforeEach
void setUp() {
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);
}
@DisplayName("Page를 간단한 querydsl으로 구현하자.")
@Test
void searchPageSimple(){
MemberSearchCondition condition = new MemberSearchCondition();
PageRequest pageRequest = PageRequest.of(0, 3);
Page<MemberTeamDto> result = memberRepository.searchPageSimple(condition, pageRequest);
assertThat(result.getSize()).isEqualTo(3);
assertThat(result.getContent()).extracting("username").containsExactly("member1", "member2", "member3");
}
- Test를 돌려보면 0 인덱스부터 시작하여 3개의 member1, member2, member3를 정상적으로 담아내는 것을 확인할 수 있었다.
- seachPageComplex도 코드는 달라보일 수 있으나 별반 다를 바 없다.
이는 count 쿼리가 생략 가능한 경우 생략하여 처리하며 다음과 같은 조건을 만족해야 한다.- 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 적을 때
- 마지막 페이지일 때
- 예를 들어 100개의 데이터가 있다고 했을 때 200개를 조회한다고 하면 이는 컨텐츠 사이즈가 페이지 사이즈보다 크기 때문에 CountQuery를 날리지 않는다. 하지만 20개를 조회하길 원하면 그때는 컨텐츠 사이즈가 더 적기 때문에 CountQuery를 날리는 것이다.
'Querydsl' 카테고리의 다른 글
[Querydsl] 스프링 데이터 JPA가 제공하는 Querydsl 기능 (1) | 2024.12.24 |
---|---|
[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 |