실전QueryDsl + 순수 Jpa -> 스프링 데이터 JPA 리포지토리로 변경
스프링 데이터 JPA - MemberRepository 생성
package study.querydsl.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import study.querydsl.entity.Member;
import java.util.List;
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(String username);
}
스프링 데이터 JPA 테스트
@SpringBootTest
@Transactional
class MemberRepositoryTest {
@Autowired
EntityManager em;
@Autowired
MemberRepository memberRepository;
@Test
public 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);
}
}
**⚡️ Spring Data JPA 는 단순한 쿼리는 인터페이스를 구현하면 자동으로 구현체를 만들어들어 주는데, QueryDsl 처럼 복잡한 쿼리의 경우는 직접 사용자 정의 인터페이스와 구현체를 만들어 스프링 데이터 레포지토리에 사용자 정의 인터페이스를 상속해야한다.
**
사용자 정의 리포지토리 사용법
1. 사용자 정의 인터페이스 작성
2. 사용자 정의 인터페이스 구현
3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
import study.querydsl.dto.MemberSearchCondition;
import study.querydsl.dto.MemberTeamDto;
import java.util.List;
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
import static org.springframework.util.StringUtils.isEmpty;
import static study.querydsl.entity.QMember.member;
import static study.querydsl.entity.QTeam.team;
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);
} }
import java.util.List;
public interface MemberRepository extends JpaRepository<Member, Long>,
MemberRepositoryCustom {
List<Member> findByUsername(String username);
}
@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");
}
스프링 데이터의 Page, Pageable을 활용해보자.
사용자 정의 인터페이스에 페이징 2가지 추가
package study.querydsl.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import study.querydsl.dto.MemberSearchCondition;
import study.querydsl.dto.MemberTeamDto;
import java.util.List;
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition,
Pageable pageable);
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition,
Pageable pageable);
}
/**
* 단순한 페이징, 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이 제공하는 fetchResults() 를 사용하면 내용과 전체 카운트를 한번에 조회할 수 있다.(실제 쿼리는 2번 호출)
fetchResult() 는 카운트 쿼리 실행시 필요없는 order by 는 제거한다.
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();
return new PageImpl<>(content, pageable, total);
}
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);
()-> countQuery.fetchCount()
//countQuery::fetchCount
스프링 데이터 라이브러리가 제공
예 ) 페이지 사이즈가 10이고, 컨텐츠 사이즈가 3이라고 하면 count 쿼리가 생략 가능하다.
offset (마지막 페이지 일 경우) = currentPage-1
전체 사이즈 (total) = (currentPage - 1) * pageSize + 컨텐츠 사이즈
로 계산됩니다.
는 스프링 데이터의 유틸리티 클래스인 PageableExecutionUtils에 있는 메서드입니다. 이 메서드는 컨텐츠 데이터와 페이지 정보를 사용하여 Page 객체를 생성합니다.
PageableExecutionUtils.getPage() 메서드는 다음과 같은 형식을 가지고 있습니다:
public static <T> Page<T> getPage(List<T> content, Pageable pageable, Supplier<Long> totalSupplier)
content: 페이지에 포함될 데이터 목록입니다.
pageable: 페이지 요청 정보를 나타내는 Pageable 객체입니다.
totalSupplier: 전체 데이터 개수를 제공하는 Supplier 객체입니다. 이 객체는 get() 메서드를 호출하여 전체 데이터 개수를 동적으로 가져올 수 있어야 합니다.
getPage() 메서드는 content, pageable, totalSupplier를 사용하여 Page 객체를 생성합니다. Page 객체는 페이지에 필요한 정보를 담고 있는 인터페이스로, 컨텐츠 데이터와 페이지 정보를 함께 제공합니다. 이를 통해 클라이언트는 페이지 관련 정보를 받아와 페이징 처리를 할 수 있습니다.
주로 getPage() 메서드는 데이터베이스나 다른 데이터 소스에서 페이징된 결과를 가져올 때 사용됩니다. totalSupplier를 통해 전체 데이터 개수를 지연 로딩하여 필요한 경우에만 데이터베이스 쿼리를 실행하고, 그렇지 않은 경우에는 생략할 수 있습니다.
@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);
}
}
http://localhost:8080/v2/members?size=5&page=2
참고
김영한 QueryDsl 강의 자료
실전QueryDsl + 순수 Jpa(1)-동적 쿼리와 성능 최적화 조회