[Spring] 실전QueryDsl + 순수 Jpa -동적 쿼리와 성능 최적화 조회 (5)

hyewon jeong·2023년 6월 10일
0

Spring

목록 보기
45/59

1. 순수 jpa 에서 QueryDsl 활용 하기

1-1준비

1-1-1. JpaQueryFactory 를 직접 주입해주는 방법

MemberJpaRepository

@Repository
public class MemberJpaRepository {


  private final EntityManager em;
  private final JPAQueryFactory jpaQueryFactory;

  public MemberJpaRepository(EntityManager em) {
    this.em = em;
    this.jpaQueryFactory = new JPAQueryFactory(em);
  }

장점 : 테스트 할때 EntityManager만 주입해주면된다.

1-1-2. JpaQueryFectory 를 스프링 빈으로 등록하여 자동 주입(@RequiredArgsConstructor)

QueryDsl3Application

@SpringBootApplication
public class QueryDsl3Application {

  public static void main(String[] args) {
    SpringApplication.run(QueryDsl3Application.class, args);
  }
  
  @Bean
  JPAQueryFactory jpaQueryFactory(EntityManager em){
    return new JPAQueryFactory(em);
  }

}

MemberJpaRepository

@Repository
public class MemberJpaRepository {
  private final EntityManager em;
  private final JPAQueryFactory jpaQueryFactory;

  public MemberJpaRepository(EntityManager em,JPAQueryFactory jpaQueryFactory) {
    this.em = em;
    this.jpaQueryFactory = jpaQueryFactory;
  }

이것을 @RequiredArgsConstructor 을 사용하면 아래와 같이 코드가 줄어든다.

MemberJpaRepository

@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
  private final EntityManager em;
  private final JPAQueryFactory jpaQueryFactory;

장점 : 코드가 줄어든다.
단점 : 별도의 스프링빈 등록이 필요하고, 테스트 때 조금 첫번째 방식보단 복잡하다.

참고: 동시성 문제는 걱정하지 않아도 된다. 왜냐하면 여기서 스프링이 주입해주는 엔티티 매니저는 실제 동 작 시점에 진짜 엔티티 매니저를 찾아주는 프록시용 가짜 엔티티 매니저이다. 이 가짜 엔티티 매니저는 실제 사용 시점에 트랜잭션 단위로 실제 엔티티 매니저(영속성 컨텍스트)를 할당해준다.

1-2. 동적 쿼리와 성능 최적화 조회 - Builder 사용

  • 동적쿼리Builder 사용 + 성능 최적화 Dto

MemberTeamDto - 조회 최적화용 DTO 추가

package study.querydsl.dto;
import com.querydsl.core.annotations.QueryProjection;
    import lombok.Data;
    @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 을 추가했다.
QMemberTeamDto 를 생성하기 위해 ./gradlew compileQuerydsl 을 한번 실행하자.

참고: @QueryProjection 을 사용하면 해당 DTO가 Querydsl을 의존하게 된다. 이런 의존이 싫으면, 해 당 에노테이션을 제거하고, Projection.bean(), fields(), constructor() 을 사용하면 된다.

회원 검색 조건

package study.querydsl.dto;
 import lombok.Data;
 @Data
 public class MemberSearchCondition {
//회원명, 팀명, 나이(ageGoe, ageLoe)
   private String username;
   private String teamName;
   private Integer ageGoe;
   private Integer ageLoe;
}

동적쿼리 - Builder + @QueryProjection 사용


//Builder 사용
//회원명, 팀명, 나이(ageGoe, ageLoe)
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) {
      BooleanBuilder builder = new BooleanBuilder();
     //StringUtils.hasText() import stirng.framework  -> null, "" 값d아닌 값을  boolean 값으로 반환
      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,
                      member.username,
                      member.age,
                      team.id,
                      team.name))
              .from(member)
              .leftJoin(member.team, team)
              .where(builder)
.fetch();
}

QMemberTeamDto 는 생성자를 사용하기때문에 필드 이름을 맞추지 않아도 된다. 타입을 맞추기 때문이다. 만약 필드 또는 프로퍼티 접근 방식이라면
member.id.as("memberId") 이라고 적어야 한다.

참고 ) 동적쿼리 - Builder + 생성자 DTO 사용

   return jpaQueryFactory
       .select
           (Projections.constructor(
               MemberTeamDto.class,member.id,member.username,member.age,team.id,team.name))
       .from(member)
       .leftJoin(member.team,team)
       .where(builder)
       .fetch();
 }

1-3. 동적쿼리와 성능 최적화 조회 - where절

//회원명, 팀명, 나이(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);
}

Builder 사용 보다 where 절이 좋은 점은?

where 절에 파라미터 방식을 사용하면 조건 재사용 가능

//where 파라미터 방식은 이런식으로 재사용이 가능하다.
public List<Member> findMember(MemberSearchCondition condition) {
     return queryFactory
             .selectFrom(member)
}
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
       teamNameEq(condition.getTeamName()),
       ageGoe(condition.getAgeGoe()),
       ageLoe(condition.getAgeLoe()))
.fetch();

1-4 . 조회 API 컨트롤러 개발

편리한 데이터 확인을 위해 샘플 데이터를 추가
샘플 데이터 추가가 테스트 케이스 실행에 영향을 주지 않도록 다음과 같이 프로파일을 설정
-테스트 케이스와 샘플케이스를 분리하기

  • 프로파일 설정
    src/main/resources/application.yml


  spring:
    profiles:
      active: local
 

테스트는 기존 application.yml을 복사해서 다음 경로로 복사하고, 프로파일을 test로 수정하자

src/test/resources/application.yml

spring:
  profiles:
    active: test

이렇게 분리하면 main 소스코드와 테스트 소스 코드 실행시 프로파일을 분리할 수 있다.

샘플 데이터 추가

@Component
@RequiredArgsConstructor
@Profile("local")
public class InitData {

  private final InitMemberService initMemberService;
 @PostConstruct // @PostConstruct 와 @Transactional 함께 사용할 수 없어 분리해서 만듬
  public void init(){
   initMemberService.init();
 }
@Component
 static class InitMemberService{
   @PersistenceContext
   EntityManager em;

   @Transactional
   public void init(){
     Team teamA = new Team("teamA");
     Team teamB = new Team("teamB");
     em.persist(teamA);
     em.persist(teamB);

     for(int i = 0; i< 100 ; i++){
       Team selectedTeam = i % 2 == 0 ? teamA : teamB ;
       em.persist(new Member("member"+i,i,selectedTeam));
     }

   }

 }

}

조회 컨트롤러

 @RestController
 @RequiredArgsConstructor
 public class MemberController {
     private final MemberJpaRepository memberJpaRepository;
     @GetMapping("/v1/members")
     public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition)
 {
         return memberJpaRepository.search(condition);
     }
}

Test 시 isEqualTo() 대신 extracting()과 containsExactly()은 언제 사용하는가?

assertThat(builder).extracting("username").containsExactly("member4") 

extracting("username"):
extracting 메서드는 객체에서 특정 필드나 속성을 추출하여 검증할 수 있도록 해줍니다. 이 경우에는 builder 객체의 username 필드를 추출하여 검증합니다. 추출된 필드 값들로 새로운 리스트가 생성됩니다.
즉 ! 추출된 필드 값들로 리스트를 반환한다.

containsExactly("member4"):
containsExactly 메서드는 추출된 필드 값들의 순서와 내용이 주어진 값들과 정확히 일치하는지를 검증합니다. 이 경우에는 추출된 username 필드 값들이 "member4" 하나만을 포함하고 있어야 합니다.

참고
김영한 QueryDsl 강의 자료

profile
개발자꿈나무

0개의 댓글