[Spring] Querydsl를 활용한 Repository와 Controller 개발
순수 JPA와 Querydsl
시작하기 전에 JPAQueryFactory
를 사용할 수 있도록 설정해줘야 한다.
main()
이 정의돼있는 QuerydslApplication
클래스에서 다음 내용을 적어주자.
JPAQueryFactory
을 스프링 빈으로 등록해주는 작업이며 EntityManager
을 파라미터로 받아 적용한다.
@SpringBootApplication
public class QuerydslApplication {
public static void main(String[] args) {...}
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
repository 이름은 MemberJpaRepository
로 정의하고, 위에서 적었던 JPAQueryFactory
를 사용하기 위해
다음과 같이 설정해주자.
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private final EntityManager em;
private final JPAQueryFactory queryFactory;
...
}
em
,queryFactory
에 대한 생성자를 별도로 생성해줘도 상관없음- 해당 코드 작성 생략을 위해
@RequiredArgsConstructor
적용
순수 JPA 코드는 간단간단하다. 필요할 땐 jpql 써서 처리해주면 된다.
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
public List<Member> findByUsername(String username) {
return em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", username)
.getResultList();
}
하지만 위 코드는 실행해보기 전까지 에러를 찾기 어렵다는 단점이 있다.
그래서 querydsl을 사용하면 다음처럼 바꿀 수 있고 런타임에 에러를 잡을 수 있다는 장점이 있다.
public List<Member> findAll_Querydsl() {
return queryFactory
.selectFrom(member)
.fetch();
}
public List<Member> findByUsername_Querydsl(String username) {
return queryFactory
.selectFrom(member)
.where(member.username.eq(username))
.fetch();
}
Builder 적용
builder를 적용해 좀 더 복잡한 조건들을 처리해보자.
Member
의 이름, 나이 그리고 Team
의 이름에 대해 조건을 걸어주기 위해 MemberSearchCondition
를 정의하자.
@Data
public class MemberSearchCondition {
private String username;
private String teamName;
private Integer ageGoe;
private Integer ageLoe;
}
그리고 엔티티가 아닌 DTO를 리턴할 용도로 MemberTeamDto
을 정의해주자.
@Data
public class MemberTeamDto {
private Long memberId;
private String username;
private int age;
private Long teamId;
private String teamName;
@QueryProjection
public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) {
this.memberId = memberId;
this.username = username;
this.age = age;
this.teamId = teamId;
this.teamName = teamName;
}
}
@QueryProjection
를 적용해 repository에서 DTO를 쉽게 생성할 수 있음
BooleanBuilder
타입인 builder를 생성하주고 조건들을 적용해주자.
다음 코드는 이름이 같은지, 나이가 범위안에 들어오는지 체크해주는 로직이다.
BooleanBuilder builder = new BooleanBuilder();
if(StringUtils.hasText(condition.getUsername())) {
builder.and(member.username.eq(condition.getUsername()));
}
if(StringUtils.hasText(condition.getTeamName())) {
builder.and(team.name.eq(condition.getTeamName()));
}
if(condition.getAgeGoe() != null) {
builder.and(member.age.goe(condition.getAgeGoe()));
}
if(condition.getAgeLoe() != null) {
builder.and(member.age.loe(condition.getAgeLoe()));
}
StringUtils.hasText()
는 null 체크를 하기위해 사용
그리고 위에서 만든 builder
를 where절에 적용해주면 끝난다.
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(builder)
.fetch();
QMemberTeamDto
생성자로 DTO를 쉽게 생성할 수 있는건@QueryProjection
를 적용했기 때문QMemberTeamDto
를 사용하기 위해 compileQuerydsl 반드시 체크
where절 파라미터 사용
builder 사용 시 단점이 있다면 builder 코드가 복잡하다는 점이 있다. 내용이 길어지면 가독성도 떨어지게 된다.
다음과 같이 조건들을 쪼개서 함수를 개별로 선언하고 where절에 적용하면 가독성도 올라가고 코드도 간단해진다.
여기서 선언한 함수는 다른 로직에서 재활용할 수 있다는 장점도 있다!
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 StringUtils.hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return StringUtils.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;
}
Predicate
보다는BooleanExpression
을 리턴 타입으로 적용할 것
controller 개발
위에서 만들었던 repository 코드를 그대로 적용만 해주면 된다.
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberJpaRepository memberJpaRepository;
@GetMapping("/v1/members")
public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) {
return memberJpaRepository.search(condition);
}
}
스프링 데이터 JPA와 Querydsl
스프링 데이터 JPA + Querydsl repository
스프링 데이터 JPA를 적용하기 위해 MemberRepository
를 생성하자.
위에서 save()
, findAll()
등은 기본으로 제공하지만 username
으로 조회하는 함수는 없으므로 만들어주자.
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(String username);
}
위에서 querydsl를 적용해 만들었던 search()
는 MemberRepository
가 interface이므로 그대로 쓸 수 없다.
때문에 사용자 정의 리포지토리가 필요하다. 일단 사용자 정의 인터페이스부터 작성하자.
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
- 이름은 마음대로 설정해도 상관없음
- 위에서 querydsl로 구현했던
search()
를 사용하기 위해 해당 인터페이스에 정의
그리고 MemberRepositoryImpl
를 생성해 방금 만든 MemberRepositoryCustom
interface를 구현해주자.
이 때 JPAQueryFactory
를 사용해야 하므로 선언해준 뒤 EntityManager
로 주입해주자.
위에서 구현했던 search()
, usernameEq()
, teamNameEq()
, ageGoe()
, ageLoe()
를 그대로 가져오자.
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 StringUtils.hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return StringUtils.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;
}
}
search()
는MemberRepositoryCustom
에 정의된 함수이므로 오버라이딩 필수- 이름은 반드시 상속할 repository 이름 뒤에 Impl이 반드시 붙어야 함
MemberRepository
를 상속할거니까MemberRepositoryImpl
로 설정
MemberRepository
에서 search()
를 사용할 수 있도록 MemberRepositoryCustom
를 상속받자!
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username);
}
Querydsl 페이징 설정
방법은 2가지가 있고 카운트 쿼리를 따로 설정해주는지 여부에 따라 달라진다.
별도로 설정해주지 않는 함수를 searchPageSimple()
, 별도로 설정해주는 함수를 searchPageComplex()
라 하자.
MemberRepositoryCustom
에 위 2개 함수를 정의해주자.
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
MemberRepositoryImpl
에 searchPageSimple()
를 정의해주자.
search()
에서 쓰는 쿼리는 같지만 페이징 처리를 위해 코드를 일부 수정했다.
@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())
)
.fetchResults();
List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total);
}
List
가 아닌Page
를 리턴함에 주의fetch()
가 아닌fetchResults()
를 적용- 내용과 전체 카운트를 한번에 조회 가능 = 쿼리 2번 호출
카운트 쿼리를 별도로 설정해주는 searchPageComplex()
를 정의해주자.
searchPageSimple()
와 거의 유사하고, 카운트 관련 코드를 수정했다.
@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())
)
.fetch();
//직접 total count 쿼리 날림
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);
}
fetchResults()
가 아닌fetch()
를 사용함에 주의total
뒤 코드를 보면 카운트 쿼리를 직접 querydsl로 만들어서 날리고 있음PageImpl
은Page
의 구현체
CountQuery 최적화
위 searchPageComplex()
에서 카운트 쿼리가 필요없는 일부 상황에 대해 처리하는 스프링 데이터 라이브러리를
활용한다면 최적화가 가능하다! total
부터 내용을 주석처리하고 다음과 같이 처리해주면 된다.
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());
- 카운트 쿼리가 필요없는 상황
- 시작 페이지에서 컨텐츠 사이즈 < 페이지 사이즈
- 마지막 페이지 일 때, 컨텐츠 사이즈 < 페이지 사이즈
PageableExecutionUtils.getPage()
를 사용하면 해당 상황들을 알아서 처리해줘서 최적화 가능
controller 개발
위에서 만든 MemberRepository
를 추가해주고 구현했던 함수를 그대로 활용해주면 된다.
페이징 처리를 위해 Pageable
를 파라미터에 넣어줘야 함에 주의!
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberJpaRepository memberJpaRepository;
private final MemberRepository memberRepository; //추가
//v1 API 생략
@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);
}
}
댓글남기기