BackEnd/JPA

[JPA] 실무 활용 - 순수 JPA와 Querydsl

샤아이인 2022. 5. 15.

내가 공부한 것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼 겸 상세히 기록하고 얕은 부분들은 가볍게 포스팅하겠습니다.

이번 글은 다음과 같은 순서로 이어질 것 이다.

 

  1. 순수 JPA 리포지토리와 Querydsl
  2. 동적쿼리 Builder 적용
  3. 동적쿼리 Where 적용
  4. 조회 API 컨트롤러 개발

 

1. 순수 JPA 리포지토리와 Querydsl

우선 순수 JPA를 기반으로 repository를 하나 만들자.

Repository
public class MemberJpaRepository {

    private final EntityManager em;
    private final JPAQueryFactory jpaQueryFactory;

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

    public void save(Member member) {
        em.persist(member);
    }

    public Optional<Member> findById(Long id) {
        Member findMember = em.find(Member.class, id);
        return Optional.ofNullable(findMember);
    }

    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();
}

java 코드로 작성하기 때문에 매우 간단하게 끝나버렸다.

 

▶ JPAQueryFactory 스프링 빈 등록

기존에는 다음과 같이 생성자를 통해 EntityManager를 받고, 이를 이용하여 JPAQueryFactory를 만들었다.

@Repository
public class MemberJpaRepository {

    private final EntityManager em;
    private final JPAQueryFactory queryFactory;

    public MemberJpaRepository(EntityManager em) {
        this.em = em;
        this.queryFactory = new JPAQueryFactory(em);
    }
    // ... 생략
}

하지만 그냥 다음과 같이 Bean으로 만들어서 등록해두고 사용해도 된다.

@SpringBootApplication
public class QuerydslApplication {

    public static void main(String[] args) {
        SpringApplication.run(QuerydslApplication.class, args);
    }

    // Bean으로 등록하기
    @Bean
    JPAQueryFactory jpaQueryFactory(EntityManager em) {
        return new JPAQueryFactory(em);
    }

}

Repository에서는 @RequiredArgsContructor를 사용하면 편하게 DI 받을 수 있다.

 

참고로 동시성 문제는 걱정하지 않아도 된다.

왜냐하면 Spring이 주입해주는 EntityManager는 실제 동작 시점에 진짜 EntityManager를 찾아주는 proxy EntityManager이다.

이 가짜 EntityManager는 실제 사용 시점에 트랜잭션 단위로 실제 EntityManager(영속성 컨텍스트)를 할당해준다.

 

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

이번시간에는 동적쿼리를 통해서 DTO를 조회해올 것 이다. 우선 사용할 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을 사용하고 있기 때문에 QMemberTeamDto 를 생성하기 위해 ./gradlew compileQuerydsl 을 한번 실행하자.

 

또한 검색 조건으로 사용할 class를 하나 만들자.

- MemberSearchCondition

@Data
public class MemberSearchCondition {
    // 회원명, 팀명, 나이(goe, loe)
    private String username;
    private String teamName;
    private Integer ageGoe;
    private Integer ageLoe;
}

 

기존의 repository에 다음과 같이 검색조건을 받아서 검색한 후, 반환해주는 메서드를 만들어보자.

public List<MemberTeamDto> searchByBuilder(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(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();
}

 

이를 테스트 코드를 통해서 검증해 보자.

@Test
public void search_builder_test() {
    // given
    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);

    // when
    MemberSearchCondition searchCondition1 = new MemberSearchCondition();
    searchCondition1.setAgeGoe(35);
    searchCondition1.setAgeLoe(40);
    searchCondition1.setTeamName("teamB");

    MemberSearchCondition searchCondition2 = new MemberSearchCondition();
    searchCondition2.setTeamName("teamB");


    // then
    List<MemberTeamDto> result = memberJpaRepository.searchByBuilder(searchCondition1);
    assertThat(result).extracting("username").containsExactly("member4");

    List<MemberTeamDto> result2 = memberJpaRepository.searchByBuilder(searchCondition2);
    assertThat(result2).extracting("username").containsExactly("member3", "member4");
}

35살 이상 40살 이하의 회원을 찾는 조건을 searchCondition으로 만들었다.

이를 searchByBuilder에 넘겨 결과를 전달받고 있다.

 

만약 검색 조건이 하나도 없이 전달됬다면 어떻게 될까?

 

모든 회원에 대한 정보를 다 조회해오게 된다. 이게 문제점이다!

회원이 적어서 지금은 다행이지만, 회원이 점차 늘어나 3만명 정도만 된다 쳐도, 조건이 없을때마다 3만건을 조회해오는 것은 무리이다.

=> 기본 조건을 지정해 주거나, limit을 걸어주는것이 좋다.

 

(Intellij tip : command + 1 -> 화면 왼쪽 디렉토리 목록리스트 숨김, 보이기 기능)

(Intellij tip : command + shift + enter -> if 조건문 까지만 입력한 후 {} 중괄호를 만들면서 커서가 중괄호 안으로 이동하는 기능)

 

3. 동적 쿼리와 성능 최적화 조회 - Where절 파라미터 사용

이번시간에는 직전에 builder를 사용하여 만든 동적 쿼리를 where절을 활용하여 만들어보자.

 

원래 BooleanExpression을 반환해야 하지만, null-safe한 코드를 만들기 위해 BooleanBuilder를 이용하였다.

public List<MemberTeamDto> searchByWhereParams(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 BooleanBuilder usernameEq(String username) {
    if(StringUtils.hasText(username)) {
        return new BooleanBuilder(member.username.eq(username));
    }
    return new BooleanBuilder();
}

private BooleanBuilder teamNameEq(String teamName) {
    if(StringUtils.hasText(teamName)) {
        return new BooleanBuilder(team.name.eq(teamName));
    }
    return new BooleanBuilder();
}

private BooleanBuilder ageGoe(Integer ageGoe) {
    if(ageGoe != null) {
        return new BooleanBuilder(member.age.goe(ageGoe));
    }
    return new BooleanBuilder();
}

private BooleanBuilder ageLoe(Integer ageLoe) {
    if(ageLoe != null) {
        return new BooleanBuilder(member.age.loe(ageLoe));
    }
    return new BooleanBuilder();
}

직전 builder를 이용했던 test 코드를 그대로 사용해도 동일하게 테스트를 통과한다.

 

추가로 다음과 같은 메서드를 만든다고 해보자.

private BooleanBuilder ageBetween(Integer ageLoe, Integer ageGoe) {
  return ageLoe(ageLoe).and(ageGoe(ageGoe));
}

기존의 ageLoe() 와 ageGoe() 메서드를 조립하여 새로운 메서드를 만들게 되었다.

또한 BooleanBuilder를 반환하도록 하여 null-safe하게 만들었기 때문에 안전한다.

ageLoe(ageLoe)가 null을 반환할 일이 없어졌기 때문에 .and 를 적용할떄 NullPointException이 발생할 일이 없다.

 

위와 같이 다양한 함수를 만들어 조립할수 있다는 점이 매우 큰 장점이다.

댓글