본문 바로가기

Backend/SpringBoot

SpringBoot의 Querydsl을 이용하여 동적쿼리(Dynamic SQL) 작성하기

안녕하세요! 이번 포스팅에서는 SpringBoot의 QueryDSL을 이용해서 동적쿼리(Dynamic SQL)를 사용해보도록 하겠습니다.

모든 코드는 Github에서 확인이 가능합니다.

 

📚 사전 준비

프로젝트 설명

여러 더미 데이터들을 넣고 동적으로 조건절을 생성하는 프로젝트를 작성해보겠습니다.

 

개발 환경

  • IntelliJ Ultimate
  • Gradle 6.0.1 / Maven
  • Java 1.8
  • SpringBoot 2.2.2
  • Window

🔎 구현

1. 의존성 추가

가장 먼저 QueryDSL을 사용하기 위해 필요한 의존성을 추가해줍니다.

 

build.gradle

plugins {
	id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

dependencies {
	compileOnly 'org.projectlombok:lombok' 
        runtimeOnly 'com.h2database:h2'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'com.querydsl:querydsl-jpa'
}

def querydslDir = "$buildDir/generated/querydsl"

/* Querydsl Q-class 추가 코드 */
querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
sourceSets {
    main.java.srcDir querydslDir
}
configurations {
    querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

의존성 추가를 한 후에

Gradle - Tasks - build - build를 실행시켜줍니다.

 

Maven인 경우에는 아래와 같이 설정합니다.

pom.xml

<dependency>
	<groupId>com.querydsl</groupId>
	<artifactId>querydsl-jpa</artifactId>
	<version>4.1.3</version>
</dependency>

...

<plugin>
	<groupId>com.mysema.maven</groupId>
	<artifactId>apt-maven-plugin</artifactId>
	<version>1.1.3</version>
	<executions>
		<execution>
			<goals>
				<goal>process</goal>
			</goals>
			<configuration>
				<outputDirectory>target/generated-sources/java</outputDirectory>
				<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
			</configuration>
		</execution>
	</executions>
	<dependencies>
		<dependency>
			<groupId>com.querydsl</groupId>
			<artifactId>querydsl-apt</artifactId>
			<version>4.1.3</version>
		</dependency>
	</dependencies>
</plugin>

Maven - Lifecycle- compile을 실행시켜줍니다.

 

2. Entity 만들기

Person.java

@Getter
@NoArgsConstructor
@Entity
public class Person {
    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Integer id;

    @Column(nullable=false)
    private String firstName;

    @Column(nullable=false)
    private String lastName;

    @Column(nullable=false)
    private Integer age;

    @Column(unique=true)
    private String email;

    public Person(String firstName, String lastName, int age, String email) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.email = email;
    }
}

Entity를 만들어줍니다.

Entity Person의 구성은 아래와 같습니다.

  • id(Primary Key) : 고유값
  • firstName, lastName : 이름
  • age : 나이
  • email : 이메일

 

3. Predicate 만들기

다음은 Predicate를 만들어줍니다. Predicate를 사용하면 동적으로 조건절을 만들 수 있습니다.

PersonPredicate.java

public class PersonPredicate {

    public static Predicate search(String firstName, String lastName){
        QPerson person = QPerson.person;

        BooleanBuilder builder = new BooleanBuilder();

        if(firstName != null){
            builder.and(person.firstName.eq(firstName));
        }
        if(lastName != null){
            builder.and(person.lastName.eq(lastName));
        }

        return builder;
    }

}

위의 예시 같은 경우에는 firstName과 lastName을 받아

null이 아닌 경우에 입력받은 값과 같다는 조건을 **Q-class(쿼리용 클래스)**를 사용하여 추가합니다.

예시를 몇 개 들어보겠습니다.

  1. search("라면", "신")
    • firstName이 '라면'이고 lastName이 '신'인 것을 찾음
    • WHERE firstName = '라면' AND lastName = '신'
  2. search("라면", null)
    • firstName이 '라면'인 것을 찾음
    • WHERE firstName = '라면'
  3. search(null, null)
    • 조건문을 추가하지 않음

 

4. Repository 만들기

PersonRepository.java

public interface PersonRepository extends JpaRepository<Person, Integer>, QuerydslPredicateExecutor<Person> {

}

Repository는 원래 JPA를 사용하여 만드는 방식으로 JpaRepository를 상속 받아 구현합니다.

하지만 Querydsl을 사용할 경우에는 QuerydslPredicateExecutor를 추가로 상속받아야 합니다.


📄 테스트

이제 구현은 끝났으니 기능이 잘 동작하는지 테스트하기 위해 PersonRepositoryTest를 생성합니다.

 

0. 테스트 전 데이터 삽입

PersonRepositoryTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonRepositoryTest {

    @Autowired
    private PersonRepository repository;


    @Before
    public void cleanUp() throws Exception {
        repository.deleteAll();

        repository.save(new Person("철수", "김", 22, "kim@chulsu.com"));
        repository.save(new Person("차차", "김", 15, "cha@cha.cha"));
        repository.save(new Person("프링", "스", 41, "spring@bo.com"));
        repository.save(new Person("자바", "김", 38, "imjava@java.net"));
        repository.save(new Person("쿼리", "최", 19, "choi@que.com"));
        repository.save(new Person("고당", "최", 76, "best@good.com"));
        repository.save(new Person("선화", "스", 11, "flower@cha.cha"));
    }

}

Querydsl을 테스트해보기 위해 필요한 더미 데이터들을 넣어주는 메소드를 작성합니다. 테스트 전에 실행해야 하므로 @Before 어노테이션을 사용합니다.

 

1. 모든 리스트 가져오기

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonRepositoryTest {

    @Autowired
    private PersonRepository repository;


    @Before
    public void cleanUp() throws Exception {
        repository.deleteAll();

        repository.save(new Person("철수", "김", 22, "kim@chulsu.com"));
        repository.save(new Person("차차", "김", 15, "cha@cha.cha"));
        repository.save(new Person("프링", "스", 41, "spring@bo.com"));
        repository.save(new Person("자바", "김", 38, "imjava@java.net"));
        repository.save(new Person("쿼리", "최", 19, "choi@que.com"));
        repository.save(new Person("고당", "최", 76, "best@good.com"));
        repository.save(new Person("선화", "스", 11, "flower@cha.cha"));
    }

	/* 추가 */
	@Test
	public void 모든_리스트_가져오기() throws Exception {
		List<Person> personList = repository.findAll();
		assertThat(personList.size()).isEqualTo(7); // 총 갯수
  	}
}

첫 번째 테스트는 모든 리스트를 가져오는 테스트입니다. findAll()을 이용해 모든 리스트를 가져온 후, 제가 아까 넣어준 더미 데이터 갯수(7개)와 같은지 비교합니다.

 

2. 김씨 성 찾기 (queryFactory 사용)

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonRepositoryTest {

    @Autowired
    private PersonRepository repository;

    /* 추가 */
    @PersistenceContext // 영속성 객체를 자동으로 삽입해줌
    private EntityManager em; 


    @Before
    public void cleanUp() throws Exception {
        repository.deleteAll();

        repository.save(new Person("철수", "김", 22, "kim@chulsu.com"));
        repository.save(new Person("차차", "김", 15, "cha@cha.cha"));
        repository.save(new Person("프링", "스", 41, "spring@bo.com"));
        repository.save(new Person("자바", "김", 38, "imjava@java.net"));
        repository.save(new Person("쿼리", "최", 19, "choi@que.com"));
        repository.save(new Person("고당", "최", 76, "best@good.com"));
        repository.save(new Person("선화", "스", 11, "flower@cha.cha"));
    }

    @Test
    public void 모든_리스트_가져오기() throws Exception {
	
        List<Person> personList = (List<Person>) repository.findAll();
	
        assertThat(personList.size()).isEqualTo(7); // 총 갯수
    }

    /* 추가 */
    @Test
    public void 김씨_성_찾기() throws Exception {

        JPAQueryFactory queryFactory = new JPAQueryFactory(em);

        QPerson person = QPerson.person;

        long result = queryFactory.selectFrom(person).where(person.lastName.eq("김")).fetchCount(); // 성이 김씨인 사람 찾기
        List<Person> result2 = queryFactory.selectFrom(person).where(person.lastName.eq("김"), person.email.like("%.com")).fetch(); // 성이 김씨이고 메일 뒤가 com으로 끝나는 사람 찾기
        QueryResults<Person> result3 = queryFactory.selectFrom(person).where(person.lastName.eq("김"), person.age.between(20,40)).fetchResults(); // 성이 김씨이고 나이가 20~40세 사이인 사람 찾기

        assertThat(result).isEqualTo(3);
        assertThat(result2.get(0).getFirstName()).isEqualTo("철수");
        assertThat(result3.getTotal()).isEqualTo(2);

    }

}

두 번째 테스트는 JpaQueryFactory를 사용한 테스트입니다. JPAQueryFactory를 사용하면 동적으로 조건절을 만들 수 있습니다. selectFrom()을 이용해서 조회할 객체(Q-class)를 정해주고, where()로 원하는 조건문을 만들어주면 됩니다.

결과를 반환하는 메소드들은 아래와 같습니다.

  • fetch() : 조회 결과가 여러 건일 때 사용
  • fetchOne() : 조회 결과가 1건만 있을 때 사용 (1건 이상이면 NonUniqueResultException 발생)
  • fetchFirst() : 무조건 첫 번째건 반환
  • fetchResults() : 조회 결과 + 개수 반환
  • fetchCount() : 조회 결과의 개수 반환

아래같이 코드를 분리하여 동적으로 사용도 가능합니다.

JPAQuery<Person> query = queryFactory.selectFrom(person);
    
// 입력받은 이름이 김이면 김씨 성을 가진 이름을, 아니면 최를 가진 이름의 리스트를 리턴
if(lastName.equals("김")){
    query = query.where(person.lastName.eq("김"));
}
else{
    query = query.where(person.lastName.eq("최"));
}
    
long result = query.fetchCount();

 

3. 스선화 찾기 (Predicate 사용)

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonRepositoryTest {

    @Autowired
    private PersonRepository repository;

    @PersistenceContext
    private EntityManager em; // 영속성 객체를 자동으로 삽입해줌


    @Before
    public void cleanUp() throws Exception {
        repository.deleteAll();

        repository.save(new Person("철수", "김", 22, "kim@chulsu.com"));
        repository.save(new Person("차차", "김", 15, "cha@cha.cha"));
        repository.save(new Person("프링", "스", 41, "spring@bo.com"));
        repository.save(new Person("자바", "김", 38, "imjava@java.net"));
        repository.save(new Person("쿼리", "최", 19, "choi@que.com"));
        repository.save(new Person("고당", "최", 76, "best@good.com"));
        repository.save(new Person("선화", "스", 11, "flower@cha.cha"));
    }


    @Test
    public void 모든_리스트_가져오기() throws Exception {

        List<Person> personList = (List<Person>) repository.findAll();

        assertThat(personList.size()).isEqualTo(7); // 총 갯수
    }


    @Test
    public void 김씨_성_찾기() throws Exception {

        JPAQueryFactory queryFactory = new JPAQueryFactory(em);

        QPerson person = QPerson.person;

        String lastName = "김";

        JPAQuery<Person> query = queryFactory.selectFrom(person);

        if(lastName.equals("김")){
            query = query.where(person.lastName.eq("김"));
        }
        else{
            query = query.where(person.lastName.eq("최"));
        }

        long result = query.fetchCount();

        //long result = queryFactory.selectFrom(person).where(person.lastName.eq("김")).fetchCount(); // 성이 김씨인 사람 찾기
        List<Person> result2 = queryFactory.selectFrom(person).where(person.lastName.eq("김"), person.email.like("%.com")).fetch(); // 성이 김씨이고 메일 뒤가 com으로 끝나는 사람 찾기
        QueryResults<Person> result3 = queryFactory.selectFrom(person).where(person.lastName.eq("김"), person.age.between(20,40)).fetchResults(); // 성이 김씨이고 나이가 20~40세 사이인 사람 찾기

        assertThat(result).isEqualTo(3);
        assertThat(result2.get(0).getFirstName()).isEqualTo("철수");
        assertThat(result3.getTotal()).isEqualTo(2);

    }


    /* 추가 */
    @Test
    public void 스선화_찾기() throws Exception {

        List<Person> result = (List<Person>) repository.findAll(PersonPredicate.search("선화", "스")); // 성이 스이고 이름이 선화인 사람 찾기
        List<Person> result2 = (List<Person>) repository.findAll(PersonPredicate.search(null, "스")); // 성이 스씨인 사람 찾기
        List<Person> result3 = (List<Person>) repository.findAll(PersonPredicate.search(null, null));

        assertThat(result.size()).isEqualTo(1);
        assertThat(result2.size()).isEqualTo(2);
        assertThat(result3.size()).isEqualTo(7);

    }
}

세 번째 테스트는 Predicate를 사용한 테스트입니다.

아까 Repository에서 JpaRepository뿐만 아니라 QuerydslPredicateExecutor를 상속받은걸 기억하시나요? QuerydslPredicateExecutor를 상속받음으로 인해 findAll() 안에 아까 만든 Predicate를 넣을 수 있게 되었습니다. (구현 - 3. Predicate 만들기에서 만든 코드입니다!) 따라서 Predicat에서 동적으로 생성한 조건절을 넣어줄 수 있습니다.


❤️ 정리

이런 식으로 Querydsl을 이용해 동적 쿼리를 생성하는 방법을 알아봤습니다. 정말 많은 삽질을 한 부분이라서 조금이라도 도움이 되었으면 하는 마음으로 포스팅을 했는데 도움이 되었는지 모르겠네요 ㅠㅠ 혹시 위 글에서 잘못된 내용이나 보충하고 싶은 내용을 알려주시면 감사하겠습니다!

읽어주셔서 감사합니다 :)


😇 참고 링크

querydsl 설정관련 질문드립니다. - 인프런