카테고리 없음

spring jpa QuerydslBinderCustomizer

머룽 2023. 4. 21. 15:26
이번 시간에 알아볼 것은 querydsl의 QuerydslBinderCustomizer을 알아볼 예정이다. QuerydslBinderCustomizer는 인터페이스이며 추상 메서드는 void customize(QuerydslBindings bindings, T root) 한개를 갖고 있다. 현재 필자는 회사나 집에서 java8을 쓰기 때문에 java8 기준으로 설명한다 아주 상세하게 컨트롤은 하지 못해도 기본적인 동적쿼리(예로 있으면 검색 아니면 검색하지 않는다)를 간단하게 만들수 있다. 뭐 기능이 얼마나 있는지는 모르겠지만 일단 필자가 테스트한 경우는 기본적인 것만 해봤기 때문에 그것만 설명을 하겠다. 예전에 querydsl를 공부할때 남겨두었던 클래스들을 재 사용했다.
public interface AccountRepository extends QueryDslPredicateExecutor<Account>,
  QuerydslBinderCustomizer<QAccount>, JpaRepository<Account, Long>, CustomAccountRepository {

  @Override
  default void customize(QuerydslBindings bindings, QAccount user) {
    bindings.bind(String.class)
      .first((StringPath path, String value) -> path.containsIgnoreCase(value));
    bindings.excluding(user.password);
  }
}
여기서 추가 된것은 QueryDslPredicateExecutor 와 QuerydslBinderCustomizer이다. JpaRepository와 CustomAccountRepository는 예전에 공부할때 쓰던거라 일단 남겨두었다. QueryDslPredicateExecutor 인터페이스는 JpaRepository 와 비슷한 추상 메서드를 갖고 있다. 메서드는 거의 비슷하지만 파라미터가 Predicate predicate 위주로 되어있다. java8은 인터페이스에서도 구현을 할 수 있어 QuerydslBinderCustomizer의 구현체를 AccountRepository에 만들었다. 구현체에 있는 것들은 실제 request를 보낼때 바인딩되어 그 바인딩된 것으로 쿼리를 하는 것이다. 말로 설명할려니 조금 힘들다. 실제로 테스트를 해보면서 알아가보자.
@RestController
@RequiredArgsConstructor
public class AccountController {

  private final AccountRepository accountRepository;

  private final ModelMapper modelMapper;

  @GetMapping("/accounts")
  public Page<AccountDto.Response> accounts(@QuerydslPredicate(root = Account.class) Predicate predicate,
                                Pageable pageable){
    Page<Account> all = accountRepository.findAll(predicate, pageable);
    List<AccountDto.Response> collect = all.getContent()
      .stream()
      .map(i -> modelMapper.map(i, AccountDto.Response.class)).collect(toList());

    return new PageImpl<>(collect, pageable, all.getTotalElements());
  }
}
어떤한 테스트를 하기 위한 controller이다. 파라미터로는 @QuerydslPredicate(root = Account.class) Predicate 와 Pageable을 받고 있다. 실제로 어떻게 되는지 테스트를 해보자. http://localhost:8080/accounts
{
  "content": [
    {
      "id": 1,
      "name": "wonwoo",
      "password": "1PassWord",
      "email": "wonwoo@test.com"
    },
    {
      "id": 2,
      "name": "wonwoo",
      "password": "2PassWord11",
      "email": "123@test.com"
    },
    {
      "id": 3,
      "name": "kevin",
      "password": "3PassWord2",
      "email": "aaa@test.com"
    },
    {
      "id": 4,
      "name": "ggg",
      "password": "PassWord33",
      "email": "bbb@test.com"
    },
    {
      "id": 5,
      "name": "ggg",
      "password": "PassWord44",
      "email": "ccc@test.com"
    },
    {
      "id": 6,
      "name": "keven",
      "password": "PassWord5",
      "email": "ddd@test.com"
    },
    {
      "id": 7,
      "name": "qqqq",
      "password": "PassWord6",
      "email": "ggg@test.com"
    }
  ],
  "totalElements": 7,
  "last": true,
  "totalPages": 1,
  "size": 20,
  "number": 0,
  "sort": null,
  "first": true,
  "numberOfElements": 7
}
파라미터를 아무것도 보내지 않았을 때 이런 결과가 있다고 가정하자. 이번에는 파라미터를 보내보자. http://localhost:8080/accounts?name=won
{
  content: [
    {
      id: 1,
      name: "wonwoo",
      password: "1PassWord",
      email: "wonwoo@test.com"
    },
    {
      id: 2,
      name: "wonwoo",
      password: "2PassWord11",
      email: "123@test.com"
    }
  ],
  totalElements: 2,
  last: true,
  totalPages: 1,
  size: 20,
  number: 0,
  sort: null,
  first: true,
  numberOfElements: 2
}
name에 won이 들어가는 것을 모두 가져왔다. 다음에는 password를 검색해보자. http://localhost:8080/accounts?password=3
{
  content: [
    {
      id: 1,
      name: "wonwoo",
      password: "1PassWord",
      email: "wonwoo@test.com"
    },
    {
      id: 2,
      name: "wonwoo",
      password: "2PassWord11",
      email: "123@test.com"
    },
    {
      id: 3,
      name: "kevin",
      password: "3PassWord2",
      email: "aaa@test.com"
    },
    {
      id: 4,
      name: "ggg",
      password: "PassWord33",
      email: "bbb@test.com"
    },
    {
      id: 5,
      name: "ggg",
      password: "PassWord44",
      email: "ccc@test.com"
    },
    {
      id: 6,
      name: "keven",
      password: "PassWord5",
      email: "ddd@test.com"
    },
    {
      id: 7,
      name: "qqqq",
      password: "PassWord6",
      email: "ggg@test.com"
    }
  ],
  totalElements: 7,
  last: true,
  totalPages: 1,
  size: 20,
  number: 0,
  sort: null,
  first: true,
  numberOfElements: 7
}
하지만 결과와 다르게 모두 출력 되었다. 그 이유는 위의 코드에서 봤듯이 password는 제외 시켰기 때문이다.
bindings.excluding(user.password);
이번에는 특정 어떠한 파라미터로 오면 그것은 containsIgnoreCase 아닌 eq로 체크 하고 싶다면 아래와 같이 하면 된다.
@Override
default void customize(QuerydslBindings bindings, QAccount user) {
  bindings.bind(user.name).first((path, value) -> path.eq(value));
  bindings.bind(String.class)
    .first((StringPath path, String value) -> path.containsIgnoreCase(value));
  bindings.excluding(user.password);
}
user.name은 like가 아닌 eq으로 해놨다. 그럼 우리는 정확한 값이 나올때만 출력된다. 아까와 동일하게 http://localhost:8080/accounts?name=won 브라우저에 쳐보자.
{
  content: [

  ],
  totalElements: 0,
  last: true,
  totalPages: 0,
  size: 20,
  number: 0,
  sort: null,
  first: true,
  numberOfElements: 0
}
그럼 위와 같은 결과를 볼 수 있을 것이다. 그렇다면 이번에는 정확하게 문자를 집어넣어서 해보자. http://localhost:8080/accounts?name=wonwoo
{
  content: [
    {
      id: 1,
      name: "wonwoo",
      password: "1PassWord",
      email: "wonwoo@test.com"
    },
    {
      id: 2,
      name: "wonwoo",
      password: "2PassWord11",
      email: "123@test.com"
    }
  ],
  totalElements: 2,
  last: true,
  totalPages: 1,
  size: 20,
  number: 0,
  sort: null,
  first: true,
  numberOfElements: 2
}
그럼 위와 같이 두건이 검색 된다. 우리는 QuerydslBinderCustomizer를 사용해서 좀더 간단하게 코딩을 할 수 있게 되었다. 물론 복잡한 것은 Custom한 레파지토를 만들어 querydsl을 사용하거나 JPQL 혹은 네이티브 SQL을 사용해야 될 것이다. 하지만 저렇게 간단하게 해결 할 수 있는 부분도 물론 있을 것이다.