본문 바로가기
Study/실전! QueryDSL

Day 4. 순수 JPA와 QueryDSL

반응형

본 포스트는 인프런 - 실전! QueryDSL의 강의를 듣고 공부한 내용입니다

https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84

 

실전! Querydsl - 인프런 | 강의

Querydsl의 기초부터 실무 활용까지 한번에 해결, 본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의 입니다. 스프링 부트와 JPA 실무 완전 정복 로드맵을 우선 확인해주세요. 로드

www.inflearn.com


1. 순수 JPA와 QueryDSL 예제

  • 순수 JPA Repository 예제
    // 순수 JPA
    @Repository
    public class MemberJpaRepository {
    
        private final EntityManager em;
        private final JPAQueryFactory jpaQueryFactory;
    
        public MemberJpaRepository(EntityManager em) {
            this.em = em;
            jpaQueryFactory = new JPAQueryFactory(em);
        }
    
        public void save(Member member) {
            em.persist(member);
        }
    
        public Optional<Member> findById(final Long id) {
            return Optional.ofNullable(em.find(Member.class, id));
        }
    
        public List<Member> findAll() {
            return em.createQuery("SELECT m FROM Member m", Member.class)
                     .getResultList();
        }
    
        public List<Member> findByUsername(final String username) {
            return em.createQuery("SELECT m FROM Member m WHERE m.userName = :username", Member.class)
                     .setParameter("username", username)
                     .getResultList();
        }
    }​
  • 작성이 잘되었는지 테스트 코드 작성
    // Test
    @ActiveProfiles("dev")
    @SpringBootTest
    @Transactional
    class MemberJpaRepositoryTest {
    
        @Autowired
        private EntityManager em;
    
        @Autowired
        private MemberJpaRepository memberJpaRepository;
    
        @Test
        void basicTest() {
            Member member = new Member("member1", 10);
            memberJpaRepository.save(member);
    
            Member findMember = memberJpaRepository.findById(member.getId()).get();
            assertThat(findMember).isEqualTo(member);
    
            List<Member> result1 = memberJpaRepository.findAll();
            assertThat(result1).containsExactly(member);
    
            List<Member> result2 = memberJpaRepository.findByUsername("member1");
            assertThat(result2).containsExactly(member);
        }
    }​

    • ActiveProfile("dev")는 강의와 다르게 MySQL 환경에서 실행시키기 위해서 profile을 나눠놨기 때문에 붙였습니다 ( 강의는 H2 database 이용 )
  • 기존 문자열로 JPQL을 정의했던 MemberJpaRepository::findAll() 메서드를 QueryDSL로 재정의
        
    // QueryDSL     
    public List<Member> findAllUsingQueryDSL() {
        return jpaQueryFactory
            .selectFrom(member)
            .fetch();
    }​

    • 여기서 member는 QMember.member에서 static import를 활용
  • findByUsername 재정의
        
    // QueryDSL
    public List<Member> findByUsernameUsingQueryDSL(final String username) {
        return jpaQueryFactory
            .selectFrom(member)
            .where(member.userName.eq(username))
            .fetch();
    }​
  • QueryDSL 장점
    • 기존의 JPQL(문자열)로 정의한 메서드는 런타임 시점에서 오류를 잡을 수 있지만, QueryDSL은 메서드 체이닝 방식으로 정의하기 때문에 컴파일 시점에서 오류를 잡을 수 있다
    • 코드 간결성
    • 심플한 파라미터 바인딩
  • 추가 팁
    • JPAQueryFactory는 멀티쓰레딩 환경에서 동작 가능하기 때문에 SpringBean으로 미리 등록해놓고 DI 받아도된다

 

2. 동적 쿼리와 성능 최적화 조회

  • MemberTeamDto 생성
    // MemberTeamDto.class
    @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을 이용해 compile된 Q-Type 클래스를 이용 ( queryDSL 컴파일 필요 )
  • 검색 조건을 위한 DTO 생성
    // 검색조건
    @Data
    public class MemberSearchCondition {
    
        private String userName;
        private String teamName;
        private Integer ageGoe;
        private Integer ageLoe;
    
    }​

2-1. BooleanBuilder를 이용한 동적 쿼리

  • 예제 코드
        
    // Repository    
    public List<MemberTeamDto> searchByBuilder(final MemberSearchCondition condition) {
    
        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 (Objects.nonNull(condition.getAgeGoe())) {
            builder.and(member.age.goe(condition.getAgeGoe()));
        }
    
        if (Objects.nonNull(condition.getAgeLoe())) {
            builder.and(member.age.loe(condition.getAgeLoe()));
        }
    
        return jpaQueryFactory
            .select(new QMemberTeamDto(
                member.id.as("memberId"),
                member.userName,
                member.age,
                team.id.as("teamId"),
                team.name
            ))
            .from(member)
            .leftJoin(member.team, team)
            .where(builder)
            .fetch();
    }​
  • 테스트 코드
    // test
    @ActiveProfiles("dev")
    @SpringBootTest
    @Transactional
    public class SearchTest {
    
        @Autowired
        private EntityManager em;
    
        @Autowired
        private MemberJpaRepository memberJpaRepository;
    
    
        @BeforeEach
        void setUp() {
            // given
            Team teamA = new Team("teamA");
            Team teamB = new Team("teamB");
    
            // when
            em.persist(teamA);
            em.persist(teamB);
    
            // given
            Member memberA = new Member("memberA", 10, teamA);
            Member memberB = new Member("memberB", 20, teamA);
    
            Member memberC = new Member("memberC", 30, teamB);
            Member memberD = new Member("memberD", 40, teamB);
    
            // when
            em.persist(memberA);
            em.persist(memberB);
            em.persist(memberC);
            em.persist(memberD);
        }
    
        @Test
        void searchTest() {
            // given
            MemberSearchCondition condition = new MemberSearchCondition();
            condition.setAgeGoe(35);
            condition.setAgeLoe(40);
            condition.setTeamName("teamB");
    
            // when
            List<MemberTeamDto> actual = memberJpaRepository.searchByBuilder(condition);
    
            // then
            assertThat(actual)
                .hasSize(1)
                .extracting("userName")
                .containsExactly("memberD");
        }
    }​
  • 실행 쿼리
    # JPQL
    /* 
    select
        member1.id as memberId,
        member1.userName,
        member1.age,
        team.id as teamId,
        team.name 
    from
        Member member1   
    left join
        member1.team as team 
    where
        team.name = ?1 
        and member1.age >= ?2 
        and member1.age <= ?3 
    */ 
    
    # SQL Query
    select
        member0_.member_id as col_0_0_,
        member0_.user_name as col_1_0_,
        member0_.age as col_2_0_,
        team1_.id as col_3_0_,
        team1_.name as col_4_0_ 
    from
        member member0_ 
    left outer join
        team team1_ 
        on member0_.team_id=team1_.id 
    where
        team1_.name=? 
        and member0_.age>=? 
        and member0_.age<=?​
  • 동적 쿼리 주의사항
    • 위 예제에서 BooleanBuilder에 조건이 전부 빠지면, 전체 테이블을 조회하게 된다 ( 성능 이슈가 발생할 수 있음 )
    • 최소 조건을 정의하거나 안되면 limit라도 주는 것이 안전하다 ( 가급적 페이징 쿼리가 같이 들어가주는게 좋음 )

 

2-2. Where절 파라미터 사용

  • 위에서 BooleanBuilder를 이용하여 동적쿼리 생성한 메서드를 Where 파라미터로 변경해보겠습니다
  • 예제 코드
        
    // Repository
    public List<MemberTeamDto> search(final MemberSearchCondition condition) {
    
        return jpaQueryFactory
            .select(new QMemberTeamDto(
                member.id.as("memberId"),
                member.userName,
                member.age,
                team.id.as("teamId"),
                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 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 Objects.nonNull(ageGoe)? member.age.goe(ageGoe) : null;
    }
    
    private BooleanExpression ageLoe(Integer ageLoe) {
        return Objects.nonNull(ageLoe)? member.age.loe(ageLoe) : null;
    }​
  • Where절 파라미터로 넘기는 동적쿼리의 장점
    • 가독성면에서 BooleanBuilder보다 좋다
    • 만들어둔 조건 메서드들이 재사용이 가능하다
    • npt(nullPointException)만 조심한다면 각 조건 메서드도 조합이 가능하다 ( BooleanExpression로 반환하는 이유 )
728x90
반응형