본문 바로가기

JPA

[JPA] 동적쿼리, QueryDSL

JPA, QueyrDSL을 통해 동적쿼리를 다루는 방법에 대해 살펴보겠습니다. 

전체 소스코드는 아래에서 확인하실 수 있습니다.

(https://github.com/LimYooyeol/blog/tree/main/dynamic-query

1. 동적쿼리가 필요한 상황

이번에도 간단하게 Member와 Team을 활용해서 문제 상황을 재현해보겠습니다. 

@Entity
@Getter @Setter
@ToString
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private Long number;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team")
    private Team team;

    public Member(){}

    public Member(String name, Long number, Team team){
        this.name = name;
        this.number = number;
        this.team = team;
    }
}
@Entity
@Getter @Setter
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;


    public Team(){}

    public Team(String name){
        this.name = name;
    }
}

 

우선 Member와 Team 엔티티는 위와 같습니다. Member와 Team의 관계가 1:N 단방향입니다. 

 

이때 회원을 조회하는 일종의 '검색필터'를 구현한다고 가정하면, 회원의 이름/번호/회원이 속한 팀 등을 바탕으로 검색할 수 있을 것입니다. 

 

이름만으로 검색할 수도 있고, 또 이름과 번호 또는 회원을 이름과 회원이 속한 팀의 이름을 바탕으로 검색할 수도 있죠. 

말로는 간단하지만, 쿼리로 구현하려면 쉽지 않습니다.

각각의 쿼리는 간단하지만, 검색 조건에 따라 하나하나 쿼리를 만들 수는 없기 때문이죠.

 

select * from Member m where m.name = '홍길동'; (회원명으로 검색)

select * from Member m where m.number = 7;      (번호로 검색)

select * from Member m where m.name = '홍길동' & m.number = 7; (회원명 & 번호)

...

 

이러한 것들을 하나하나 if(name != null) {query += "where ~"} 이런 식으로 처리하려면 상당히 복잡하고 가독성도 떨어질 것입니다.

 

JPA에서는 QueryDSL을 이용하면 검색 조건에따른 동적 쿼리를 간단하게 생성할 수 있습니다.

 

2. 세팅

QueryDSL은 편리하지만, QueryDSL 을 사용하기 위한 세팅이 조금 복잡합니다. 

 

(1) build.gradle

먼저 build.gradle에서 의존성을 추가해주고, 빌드를 위한 설정들을 조금 추가해줘야 합니다. 

https://github.com/LimYooyeol/blog/blob/main/dynamic-query/build.gradle 를 참고해주시면 됩니다. 

 

Spring Boot 버전이나 QueryDSL 버전에 따라 설정이 달라질 수 있습니다!

 

(2) 어노테이션 프로세서

settings - Build, Execution, Deployment - Complier - Annotation Processcors 에서 Enable annotation processing을 체크해주어야 합니다. 

(3) compileJava

Gradle에서 Tasks/other/compileJava를 실행시켜주면, 사용할 Q 클래스들이 생성됩니다 ex)QMember, QTeam 

 

 

3. 동적쿼리 작성하기

기본적인 삽입 등의 코드는 생략하고 동적쿼리로 회원을 조회하는 코드만 살펴보겠습니다.

 

먼저 회원의 이름이나 번호를 검색조건으로 조회하는 경우입니다. 

기본적인 코드의 구성은 위와 같습니다.

1. JPAQuery 객체 생성 - 제네릭 클래스는 결과 타입, 생성자에 EntityManager를 주입

2. Q 클래스 생성

3. 쿼리 작성

쿼리에서 눈 여겨 볼 부분은 where 절입니다. 

언뜻 보기에는 그냥 where name like 'name' and number = 'number' 처럼 생긴 구조이지만, 여기서 parameter로 넘어가는 값이 null일 경우, where 조건에 포함하지 않습니다.

 

우선 nameLike나 numberEq 메서드를 먼저 보겠습니다. 

기본적으로 BooleanExpression을 반환하면 되고, 입력되는 값이 null일 경우에는 그대로 null을 반환합니다. 그 외에는 검색 조건에 따라 eq나 like 등 Query DSL 에서 제공하는 메서드를 통해 BooleanExpression을 반환하면 됩니다.

 

이때 Q 클래스를 검사 대상으로 검사하면 됩니다.

 

핵심은 where의 파라미터로 전달되는 값이 null인 경우에는 where 절에 포함하지 않는 것입니다! 

이후에는 포함할 조건에 따라 Query DSL 알아서 적절한 쿼리를 생성해주는 것으로, 상당히 편리하게 동적쿼리를 처리할 수 있습니다.

 

한 단계 더 나아가서 팀의 이름까지 검색 조건으로 둔다면 어떻게 될까요?

앞서 구현한 findMembersV1 에서는 where 절에 member만 이용되고 있습니다. 따라서 Team까지 함께 검색 조건으로 두려면, join을 이용해야 합니다. 

조회한 멤버 목록에 Team까지 한번에 주입하도록, fetchJoin까지 추가해두었습니다.

(※ 사실 findMembersV1도 fetchJoin을 추가해주지 않으면 N+1 문제가 발생합니다!)

결국 연관된 엔티티를 바탕으로 검색 조건을 추가할 때에도, join 이후에 똑같이 비교 함수를 작성해주고, where절에 넣어주면 됩니다. 

다만 teamNameLike(~)에서 볼 수 있듯이, 그때 비교대상은 join할 클래스의 Q 클래스가 되어야 합니다.

 

4. 테스트 

마지막으로 동적쿼리가 잘 생성되어, 검색 필터가 잘 동작하나 테스트 코드를 작성해보겠습니다. 

 

findMembersV1 먼저 테스트 해보겠습니다. 팀 하나와 3명의 회원을 추가한 후, 이름과 번호가 둘 다 null 인 경우, 이름과 번호가 모두 조건이 걸린 경우를 테스트해보았습니다. 

첫번째 리스트가 아무 조건이 없는 경우로 3명의 회원이 모두 조회된 것을 확인할 수 있고, 두번째 리스트가 이름에 '동'이 들어가고 번호가 2인 경우를 조회한 것으로, 한 명의 회원만 조회된 것을 확인할 수 있습니다.

 

다음으로 findMembersV2 까지 테스트해보겠습니다. 

    @Test
    public void 이름_번호_팀명_검색_테스트(){
        // 팀, 멤버 추가
        Team team1 = new Team("팀 1");
        Team team2 = new Team("팀 2");
        teamRepository.save(team1);
        teamRepository.save(team2);


        Member member1 = new Member("홍길동", 1L, team1);
        Member member2 = new Member("김갑동", 2L, team2);
        Member member3 = new Member("임꺽정", 3L, team1);
        memberRepository.save(member1);
        memberRepository.save(member2);
        memberRepository.save(member3);

        // 영속성 컨텍스트 초기화
        em.clear();

        // 이름에 "동"이 들어가고,
        // 번호는 상관없으며,
        // 팀 이름에 2가 들어가는
        // 멤버 검색
        List<Member> findMembers = memberRepository.findMembersV2("동", null, "2");

        System.out.println(findMembers);
    }

이번엔 팀 2개, 회원을 3명 추가한 상태로 테스트 해보겠습니다. 

중간에 번호를 null로 설정하여, 이름에 "동"이 들어가고, 번호는 상관없으며 팀 이름에 2가 들어가는 멤버를 검색하도록 테스트해보았습니다. 

검색 조건에 맞는 한명의 회원만 조회된 것을 확인할 수 있습니다. 

 

 

'JPA' 카테고리의 다른 글

[JPA] default_batch_fetch_size 동작  (0) 2023.04.12
[JPA] N+1 해결하기 (2) - OneToMany  (0) 2023.04.12
[JPA] N+1 문제 해결하기  (0) 2023.04.05
[JPA] 즉시로딩, 지연로딩  (0) 2023.03.29