[Spring] QueryDSL 오류 - org.springframework.dao.InvalidDataAccessApiUsageException: No sources given

2022. 6. 23. 17:13Web/Spring

검색 기능을 만들면서 QueryDSL로 쿼리를 만들어서 조회하는 기능을 구현 하였다.

일단 테스트를 먼저 해봐야하니까 테스트 코드를 작성하기 시작하였다. 

 

코드를 작성하고 이제 run 하면 되는데 다른 Test 때와 다르게 아이콘이 아래와 같이 되어 있었다.

 

무엇인가 문제가 있는 거 같았고.. 역시 단위 테스트를 돌려봤을 때 오류가 났다.

 

에러 난 부분을 따라가 보니 QueryDSL로 작성한 부분에서 전부 오류가 난 것을 확인해볼 수 있었다. 

 

gradle로 QueryDSL 설정 build가 complete 된 것을 어제 확인하고 잤는데 아예 잘못 작성했나 하는 생각이 들었다. 일단 QueryDSL 예제를 조금 더 살펴봐야겠다. 

 

구글링을 해보다가 stackoverflow에서 동일한 컨솔 오류를 확인하였다.

https://stackoverflow.com/questions/41747549/querydsl-exception-in-thread-main-java-lang-illegalargumentexception-no-sou

 

확인을 해보니까 QueryDSL 사용 문법이 잘못된 것을 확인할 수 있었다. select 단독이 아닌 selectFrom을 통해 DB를 조회하는 것을 확인할 수 있었다.

 

select를 사용하려고 한다면 from을 적었어야 하는데 안써서 문법 오류가 난 거 같다. 

public List<Post> findPostsByContent(String word) {
        return jpaQueryFactory.select(post)
                .where(post.contents.like(word))
                .fetch();
    }

다시 고친 코드는 아래와 같다. 

public List<Post> findPostsByContent(String word) {
        return jpaQueryFactory.selectFrom(post)
                .where(post.contents.like(word))
                .fetch();
    }

 

QueryDSL 기초적인 문법을 잘 몰랐나 하는 생각이 들었고 이참에 한번 정리해보려고 한다. 

QueryDSL의 공식 사이트를 한 번 들어가봤다.  

http://querydsl.com/static/querydsl/3.4.2/reference/html/ch02s03.html#d0e1050

http://querydsl.com/

기본적인 사용 쿼리 예시를 볼 수 있었고 아래와 같다. 

 

- Basic Query

List<Person> persons = queryFactory.selectFrom(person)
  .where(
    person.firstName.eq("John"),
    person.lastName.eq("Doe"))
  .fetch();

 

- Subqueries

List<Person> persons = queryFactory.selectFrom(person)
  .where(person.children.size().eq(
    JPAExpressions.select(parent.children.size().max())
                  .from(parent)))
  .fetch();

 

- Order

List<Person> persons = queryFactory.selectFrom(person)
  .orderBy(person.lastName.asc(), 
           person.firstName.desc())
  .fetch();

 

- Tuple Projection

List<Tuple> tuples = queryFactory.select(
    person.lastName, person.firstName, person.yearOfBirth)
  .from(person)
  .fetch();
          

 

이렇게 기본적으로 4가지가 나왔다. 나머지 3개는 이해가 갔는데 Tuple Projection은 어떤거지 하는 생각이 들었다.  검색을 조금 더 해보니 QueryDSL에서 값을 받을 때 DTO나 Tuple을 이용하고 있는 예시를 많이 볼 수 있었다.

https://stackoverflow.com/questions/12427665/multiple-columns-in-querydsl

https://www.tabnine.com/code/java/methods/com.querydsl.core.Tuple/get

 

2개 이상의 Projection인 경우에 Tuple을 사용한다고 한다. DTO를 쓰면 되지 않나 하는 생각이 들었고, Tuple을 어떤 때에 사용하는지 조금 더 살펴보고 싶었다. 

 

그 전에 Projection이 뭔지 몰랐고.. Projection을 검색해봤다. 다 비슷하게 누군가도 Projection에 대해 궁금했나 보다. 

https://stackoverflow.com/questions/3461099/what-is-a-projection

 

What is a Projection?

What is a Projection, in terms of database theory and NHibernate when using SetProjection()?

stackoverflow.com

 

대수와 관련해서 필요한 attribute만 반환할 수 있다는 이야기였다. 가령 user에 대한 select문을 작성할 때 특정한 칼럼만 가져오고 싶다면 tuple을 사용하여 가져올 수 있었다. 인용을 하자면 select DISTINCT 와 비슷한 것이다. 조회를 할 때 중복을 제거하고자 한다면? Tuple을 쓰는 것이 효과적일 수도 있겠다는 생각이 들었다. 

 


다시 원래 부분으로 돌아가서 검색하는 부분이라면 굳이 모든 정보를 가져올 필요가 없으니 PostDto와 ItemDto를 생성하고 필요한 정보만 가져와서 Post와 Item으로 반환하는게 아니라 각 Dto로 반환하는 것으로 구조를 변경해 보려고 한다. 

 

PostDto를 조금 수정하였고 코드는 아래와 같다.

package com.house.start.domain.dto.Post;

import com.house.start.domain.UploadFile;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class PostDto {

    private String contents;
    private UploadFile uploadFile;
    private LocalDateTime postDate;
}

 

자세한 내용은 필요가 없고 uploadFile과 게시일만 보이면 되니까 3개의 필드만 생성하였다. 다시 생각해보면 contents도 필요할까 생각이 들었는데 필요 따라서 빼야겠다. 

 

그러고 Repository에서 return Type 을 DTO로 변경했다. 리턴 타입만 변경하면 되는거 아닌가? 하는 생각이 들었는데 그렇게 하면 계속 오류가 났다.

오류가 난 화면

Tuple로 값을 받자니 일일히 하나씩 꺼내서 넣어줘야하는 번거로움이 있고 Dto로 반환하면 안되나 하는 생각이 들었는데 Projections를 사용하여 반환할 수 있다고 한다. 

 

http://querydsl.com/static/querydsl/4.2.1/reference/html/ch03s02.html

여기서 보면 Projections를 Bean을 통해 사용하는 것과 constructor를 이용하여 사용하는 방법이 나와 있다. 

 

Bean population 예제 코드는 아래와 같다. 

// Bean population
List<UserDTO> dtos = query.select(
    Projections.bean(UserDTO.class, user.firstName, user.lastName)).fetch();

 생성자는 아래와 같다. 

// Constructor Usage 
List<UserDTO> dtos = query.select(
    Projections.constructor(UserDTO.class, user.firstName, user.lastName)).fetch();

 

 

위에 언급한 2개를 사용하지 않고도 사용할 수 있는 방법이 있다고 하는데 @QueryProjection이라는 어노테이션이다. Dto에다가 이를 사용하면 Dto로 반환할 수 있다고 한다. 

class CustomerDTO {

  @QueryProjection
  public CustomerDTO(long id, String name) {
     ...
  }

}

 

이렇게 생성하고 사용은 아래와 같이 할 수 있다. 

QCustomer customer = QCustomer.customer;
JPQLQuery query = new HibernateQuery(session);
List<CustomerDTO> dtos = query.select(new QCustomerDTO(customer.id, customer.name))
                              .from(customer).fetch();

 

@QueryProject은 나중에 해보기로 하고 먼저 Bean Projection을 활용하여 코드를 변경해봤다. 

 public List<PostDto> findPostsByContent(String word) {
        return jpaQueryFactory.select(Projections.bean(PostDto.class, post.contents, post.uploadFile, post.postDate))
        		.where(post.contents.like(word))
                .fetch();
    }

이렇게 썼는데 처음에 있었던 오류 그대로가 났다. 

다시 검색을 해보니 Projection을 사용하기 위해서는 from을 붙여야 한다.

    public List<PostDto> findPostsByContent(String word) {
        return jpaQueryFactory.from(post)
                .select(Projections.bean(PostDto.class, post.contents, post.uploadFile, post.postDate))
                .where(post.contents.like(word))
                .fetch();
    }

 

 

다시 고쳐봤고 테스트도 통과 했다!