일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Docker
- 자격증
- QueryDSL
- jpa
- codebuild
- spring
- ec2
- CodeCommit
- codedeploy
- Spring Boot
- 오블완
- bootcamp
- 티스토리챌린지
- aws
- rds
- goorm x kakao
- MSA
- Redis
- goorm
- orm
- mapping
- DynamoDB
- sqs
- 백엔드
- serverless
- s3
- 스터디
- backenddeveloper
- CICD
- 개발자
- Today
- Total
gony-dev 님의 블로그
[Querydsl] 순수 JPA와 Querydsl 본문
지난 시간에는 querydsl을 이용하여 원하는 프로젝션 대상에 따라 결과를 효율적으로 반환하는 방법을 배웠다.
이번에는 동적 쿼리와 성능 최적화를 통한 조회에 대해 배워보겠다.
순수 JPA 리포지토리와 Querydsl
EntityManager와 Querydsl로 동일한 쿼리를 작성했을 때를 비교해보자!
다음은 리스트 전체를 조회하는 "findAll()"과 username에 따른 결과를 조회하는 "findByUsername()"을 각각 순수 JPA와 Querydsl로 구현해놓은 것이다.
public List<Member> findAll(){
return em.createQuery("select m from Member m", Member.class).getResultList();
}
public List<Member> findAll_Querydsl(){
return queryFactory
.selectFrom(member)
.fetch();
}
public List<Member> findByUsername(String username){
return em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", username)
.getResultList();
}
public List<Member> findByUsername_Querydsl(String username){
return queryFactory
.selectFrom(member)
.where(member.username.eq(username))
.fetch();
}
- Querydsl은 컴파일 지점에 오류가 나기 때문에 오류를 사전에 잡아낼 수 있는 특징이 있다!
또한 코드도 훨씬 간단하고, 동적쿼리에서 JPA의 파라미터를 넣어주기 위해 쿼리를 작성하고 "setParameter()" 설정해주어야 하는 것과 달리, Querydsl은 기본적으로 파라미터 바인딩을 지원하기에 편리하다.
❗️JPAQueryFactory에 대한 동시성 문제❗️
같은 JPAQueryFactory 객체를 모든 멀티스레드에서 사용한다면 동시성 문제가 발생할지도 모른다는 의문이 들 수도 있다.
하지만 걱정할 것 없다!
- JPAQueryFactory는 전적으로 EntityManager에 의존하는데 스프링과 엮어서 사용하게 될 경우, 동시성 문제랑 전혀 연관없이 트랜잭션 단위로 분리가 되어 동작을 하게 된다!
- 또한 EntityManager는 진짜 영속성 컨텍스트 EntityManager가 아니라 프록시 객체를 주입해주기에 다 다른 곳으로 바인딩 되도록 라우팅을 해주게 되기에 문제가 없다!
동적 쿼리와 성능 최적화 조회 - Builder 사용
동적 쿼리를 성능 최적화 조회하여 DTO로 한 번에 조회하여 보겠다!
방식은 입력되는 값으로 MemberSearchCondition 클래스와 반환되는 결과로 MemberTeamDto를 만들어볼것이다.
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;
}
}
- MemberTeamDto를 생성자와 "QueryProjection"을 사용하여 Q파일로 편리하게 사용하고 컴파일 시점에 오류를 검출할 수 있게 하였다! 하지만 Querydsl에 의존성이 강해진다는 단점이 존재한다!
MemberSearchCondition.class
@Data
public class MemberSearchCondition {
private String username;
private String teamName;
private Integer ageGoe;
private Integer ageLoe;
}
MemberJpaRepository.class
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition){
BooleanBuilder builder = new BooleanBuilder();
if (hasText(condition.getUsername())) { // 넘어온 값에 대해 ""가 아니거나 올바론 텍스트 형식을 갖지 않을 때 사용
builder.and(member.username.eq(condition.getUsername()));
}
if (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()));
}
return queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName"))
)
.from(member)
.where(builder)
.leftJoin(member.team, team)
.fetch();
}
- BooleanBuilder를 이용하여 다중 파라미터에 대한 조건을 설정해주었다.
MemberJpaRepositoryTest.class
@DisplayName("BooleanBuilder를 통해 조건에 맞는 결과를 반환한다.")
@Test
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 = memberJpaRepository.searchByBuilder(condition);
assertThat(result).extracting("username").containsExactly("member4");
}
- BooleanBuilder를 이용하여 성능 최적화를 한 메서드의 결과가 옳게 출력됨을 알 수 있었다!
동적 쿼리와 성능 최적화 조회 - Where절 파라미터 사용
Builder 패턴과 같은 방식으로 하되, BooleanBuilder가 아닌 BooleanExpression을 사용하여 성능 최적화를 진행해보자!
MemberTeamDto와 MemberSearchCondition은 그대로 사용하여 진행한다.
MemberJpaRepository.class
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;
}
- BooleanExpression을 사용하여 쿼리를 작성해보았다.
- Test는 메서드만 교체해서 사용하였기 때문에 생략하였다.
Test 결과, BooleanBuilder를 이용한 것과 동일한 결과를 추출하였다.
BooleanBuilder vs. BooleanExpression
이 둘은 성능 최적화를 한다는 점에서는 동일하지만 생성하는 방식과 표현이 다르다.
각 방법의 특징을 알아보자.
1. BooleanBuilder
- 동적 조건 용이 | BooleanBuilder는 동적으로 조건을 추가하거나 제거하는 데에 유리하여 복잡한 쿼리에 적합하다.
- 가독성 | 코드를 점차 추가하는 방식이기에 가독성이 높다.
- 초기값 설정 가능 | 생성자에서 초기 조건을 설정할 수 있기에 기본 조건과 동적 조건을 조합하기 쉽다!
- 비효율성 | 조건을 Predicate로 관리하기에 조건이 많아질수록 오버헤드가 발생할 수 있다!
2. BooleanExpression
- 고성능 | 간결하고 최적화된 SQL을 생성하여 불필요한 오버헤드가 적다!
- 체이닝 | 조건 조합 시 체이닝 메서드를 사용해 직관적이고 깔끔한 코드를 작성할 수 있다!
- 재사용성 | 변수로 추출할 수 있기에 다른 메서드에서 재사용을 할 수 있다!
- 초기값 | 동적 조건 조합 시 초기값을 null로 설정하고 추가 조건을 설정해야 한다.
실무에서는 BooleanExpression을 더 많이 선호한다고 한다.
실제로 사용해보니 재사용성과 최적화된 SQL 측면에서 많은 이점을 가져간다는 것을 알 수 있었다..!
하지만 무조건 좋다는 것은 아니며 상황에 맞게 사용하는 것이 좋은 방법이라고 생각한다.
'Querydsl' 카테고리의 다른 글
[Querydsl] 스프링 데이터 JPA가 제공하는 Querydsl 기능 (1) | 2024.12.24 |
---|---|
[Querydsl] 스프링 데이터 JPA와 Querydsl (0) | 2024.12.21 |
[Querydsl] 중급 문법 (2) | 2024.12.03 |
[Querydsl] 기본 문법 (0) | 2024.11.25 |
[Querydsl] 초기 설정하기(Spring boot 3.x.x 이상) (1) | 2024.11.17 |