스프링 데이터 JPA를 이용하면 순수 JPA를 이용해서 만들었던 기본적인 기능들은 제공해주므로 간단해진다.
public interface MemberRepository extends JpaRepository<Member, Long> {
// select m from Member m where m.username = ?
List<Member> findByUsername(String username);
}
기본적인 save() 메서드나 findById()와 같은 메서드들은 모두 스프링 데이터 JPA에서 제공해준다.
복잡한 쿼리를 쓰려면 사용자 정의 레포지토리를 작성해야 한다.
먼저 JpaRepository를 상속한 스프링 데이터 JPA 레포지토리인 MemberRepository(어쨋든 내가 만들고자 하는 엔티티의 레포지토리)를 만들고 복잡한 쿼리를 담당한 MemberRepositoryCustom 인터페이스를 만들고 여기에 선언을 해준다.
그 후 실제 구현을 담당한 MemberRepositoryImpl이라는 클래스를 만들고 MemberRepositoryCustom을 상속한다.
이때 이름이 스프링 데이터 JPA 레포지토리 이름을 따라가야 실제 구현체를 찾을 수 있으므로 이름에 조심하자.
위 과정을 코드로 보면 아래와 같다.
스프링 데이터 JPA - MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom{
// select m from Member m where m.username = ?
List<Member> findByUsername(String username);
}
MemberRepositoryCustom interface
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
MemberRepositoryImpl
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.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())
)
.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.loe(ageLoe) : null;
}
private BooleanExpression ageBetween(Integer ageLoe, Integer ageGoe) {
return ageLoe(ageLoe).and(ageGoe(ageGoe));
}
}
스프링 데이터 JPA에 있는 페이징 기능을 Querydsl에서 활용하는 방법을 소개한다.
먼저 사용자 정의 인터페이스에 페이징을 할 수 있는 메소드를 추가한다
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable); // 새로 추가한 페이징 메소드 1
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable); // 새로 추가한 페이징 메소드 2
}
전체 카운트를 한번에 조회하는 단순한 방법 - searchPageSimple()
@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();
List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total);
}
데이터와 전체 카운트를 별도로 조회하는 방법 - searchPageComplex()
@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();
long total = queryFactory
.selectFrom(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);
}
count 쿼리가 생략 가능한 경우가 있고, 이를 스프링 데이터 JPA에서 지원해주기도 한다.
일단.. 모든 경우에 count 쿼리가 필요하지 않을 때가 있다.
Count Query 최적화
@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();
JPAQuery<Member> countQuery = queryFactory
.selectFrom(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);
}
이렇게 최적화를 통해 불필요한 count 쿼리 실행 방지 → 성능 향상
이 메서드는 페이징된 결과를 반환한다. 이 과정에서 페이지 정보를 계산하기 위해 총 레코드 수를 알아야 할 때가 있다. 하지만 모든 경우에 총 레코드 수를 계산할 필요는 없음.
메서드 참조 (countQuery::fetchCount)와 람다 표현식 (() → countQuery.fetchCount())는 LongSupplier 인터페이스를 구현하는 방법이다. LongSupplier는 long getAsLong() 메서드를 가지고 있는데, 이는 총 레코드 수를 계산할 때 호출된다.
메서드 참조가 필요한 이유
만약 countQuery.fechCount()를 직접 전달한다면, 즉시 호출되어 long 값을 반환한다. 하지만 PageableExecutionUtils.getPage는 나중에 필요할 때만 총 레코드 수를 계산하기 위해 이 값을 사용하고 싶어 한다 → 이를 위해 메서드 참조나 람다 표현식을 사용하여 countQuery.fetchCount()를 나중에 호출되도록 설정하는 것!!
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);
content : 페이지에 표시될 데이터 리스트 (결과값)
pageable : 페이지 정보 (페이지 번호, 크기)
countQuery::fetchCount : 총 레코드 수를나중에 계산하기 위한 메서드 참조
결국, PageableExecutionUtils.getPage는 필요할 때 이 메서드 참조를 호출하여 총 레코드 수를 계산한다.
@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);
}