개발자로 후회없는 삶 살기

spring PART.Querydsl 본문

[백엔드]/[spring | 학습기록]

spring PART.Querydsl

몽이장쥰 2023. 5. 18. 00:27

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/roadmaps/373

 

우아한형제들 최연소 기술이사 김영한의 스프링 완전 정복 - 인프런 | 로드맵

Spring, MVC 스킬을 학습할 수 있는 개발 · 프로그래밍 로드맵을 인프런에서 만나보세요.

www.inflearn.com:443

 

본론

- Querydsl 소개

갑자기 요구사항이 추가됐습니다. 나이, 이름의 검색 조건을 추가해야합니다. 너무 쉽습니다. sql에 where로 나이와 이름 넣으면 됩니다.

 

그런데 버그가 발생했습니다. 문자를 합쳐보면 member 뒤에 띄어쓰기가 없어서 memberwhere가 됩니다. 퀴리는 문자라서 실행해보기 전까지 작동 여부를 확인하기 어렵다는 문제가 있습니다. 컴파일 시점에 에러가 발생하지 않아서 고객이 사용하는 실행하기 전에는 절대로 알 수 없습니다. 만약 이 쿼리가 컴파일 시점에 오류가 터졌으면 바로 잡았을 것입니다. 

 

 

또한 sql은 작성할 때 컬럼명이 10 ~ 20개가 있으니 다 외울 수가 없습니다. 근데 만약 sql이 클래스처럼 타입이 있고 자바 코드로 작성할 수 있다면 어떨까요? 그렇게 되면 ide 자동 완성기능으로 username이라면 us만 쳐도 완성이 될 수 있습니다. 오타나면 다 오류 잡아줄 수 있고 이런 것을 type-safe라고 합니다. 

 

-> QueryDSL

이런식으로 하면 자동 완성되고 컴파일 오류도 됩니다. 이게 쿼리 dsl이 쿼리를 마치 자바 코드로 짜듯이 type safe하게 개발할 수 있도록 지원하는 프레임 워크입니다. 쿼리를 자바 코드로 작성할 수 있게 도와주는 것으로 sql을 작성할 때 컴파일 오류를 볼 수 있습니다.

 

-> 예시

20 ~ 40세, 김씨, 나이 많은 순서, 3명 출력 조건이 있습니다. 테이블과 엔터티가 이렇게 있으면 jpa에서 쿼리 하는 방법은 3가지가 있습니다.

 

1. jpql

 

순수 jpa에서 사용한 것 처럼 jpql을 작성하고 createQuery하고 3명 조회이므로 페이징이 필요하니 set으로 하면 Limit가 쿼리에 들어가고 getResultList하면 List로 반환이 됩니다. jpql은 쿼리가 익숙하지만, type safe하지 않아 동적쿼리하기 어렵습니다.

 

2. criteria

그래서 jpa에서 criteria를 제공하며 자바 코드로 쿼리를 작성할 수 있게 됩니다. 근데 코드가 너무 길고 복잡합니다. 또한 age, name이 type-safe하지 않습니다.

 

- QueryDSL

그래서 쿼리 dsl이 나옵니다. 쿼리 dsl은 처음에 sql, jpql을 떠나서 쿼리 전체를 추상화해보기 위해 나온 것입니다. 다양한 기술에 대해 쿼리를 추상화하고 자바 코드로 쓸 수 있게 하기 위해 나왔습니다. jpa, 몽고 같은 기술을 type-safe하게 sql을 만들어주는 프레임워크입니다.

 

그러면 QueryDSL를 하려면 코드가 필요한데 테이블이나 엔터티에서 정보를 뽑아내서 코드 생성기가 돌아서 qmember인 쿼리용 멤버 객체를 만들어 줍니다. 코드 생성기인 Annotaion Processing Tool이라는게 있어서 이게 JPA 엔터티 코드를 읽어서 q 멤버를 만듭니다. 상품 엔터티면 qitem 객체를 만들고 그걸 가지고 쿼리를 자바 코드로 작성할 수 있게 합니다.

 

- QueryDSL jpa

다양한 기술을 위해 추상화 했다고 했는데 QueryDSL jpa는 jpa쿼리를 typesafe하게 작성하는데 많이 사용됩니다.

 

-> 예시

아까 그 조건을 다시 해봅니다. jpa에서 Member.java 엔터티를 보고 APT를 실행해서 qmember.java 코드가 만들어지고 Member.java와 같은 속성을 가지고 있습니다. 

 

-> 쿼리 작성법

쿼리.select.from.where가 되고 m.age.between(20, 40)이 되고 .like 이러넥 됩니다. 쿼리를 자바 코드로 작성할 수 있게 됩니다. 이것으로 jpql이 만들어집니다. 

 

 

QueryDSL로 작성하면 이게 jpql이 생성되고 그것을 sql로 번역해서 실행합니다. QueryDSL jpq는 QueryDSL를 가지고 jpql을 만들어주는 빌더라고 생각하면 됩니다. 결국 jpa, jpql을 잘 알아야합니다. 

 

그러면 단순하고 type safe하고 런타임오류를 잡습니다. 하지만 APT를 셋팅해야합니다.

 

-> 동적쿼리

불린 빌더라는게 있는데 빌더에 동적 조건을 넣어 놓고 쿼리할 때 where에 빌더를 넣으면 동적쿼리가 자동으로 만들어집니다. 조인도 가능하고 페이징, 정렬도 다 지원합니다. 실무에서는 다 스프링 데이터 JPA를 사용하는데 약점이 조회였습니다. 그 부분을 QueryDSL로 보완하면 코딩이 재밌어집니다. 

 

> 단순한 경우는 스프링 데이터 JPA를 쓰고 복잡한 경우 QueryDSL를 쓰면 됩니다. 그래도 안되는 경우가 있는데 그때는 네이티브 쿼리를 사용해야하고 탬플릿과 Mybatis를 사용하면 됩니다.

 

- 퀴리디 설정

그래들에 추가합니다. ATP를 추가해서 엔터티 어노테이션을 읽어서 q 멤버를 만드는 것입니다. 근데 이 방법은 환경에 따라서 동작이 달라집니다. 빌드할 때 인텔리로 할 지 그래들로 실행할지에 따라 다른데 

 

1) Gradle

그래들을 선택하면 QueryDSL를 쓰면 q멤버 클래스를 만들어야하는데

그래들에서 clean으로 q멤버 빌드한 것을 초기화하고 

 

그리고 other에 compile.java를 해야합니다. 이렇게 하면 빌드 폴더에 ATP 밑에 Q 파일이 생깁니다. 패키지는 원래 엔터티 객체의 위치와 같습니다. 이것을 우리 코드에서 사용합니다. clean을 하면 빌드 폴더가 삭제됩니다.

 

2) 인텔리제이 

인텔리제이는 그래들을 거치치 않고 바로 자바를 실행하는 것으로 그냥 app 클래스 main 함수를 실행하면 됩니다. 이렇게 하면 generated라고 생깁니다. 이렇게 q 파일이 생깁니다. 이제 이것을 가지고 코딩을 합니다.

 

 

이렇게 하면 main에 생겨서 gitignore도 해야하고 삭제할 때도 수동으로 지우지 않기 위해 clean을 그래들에 설정합니다.

 

- QueryDSL 적용 

이제 QueryDSL를 우리 어플에 적용해보겠습니다. 역시 아이템 레포 v3 구현체를 만듭니다. OCP를 위해서 그렇습니다.

 

@Repository
@Transactional
public class JpaRepositoryV3 implements ItemRepository {
    private final EntityManager em;
    private final JPAQueryFactory query;

쿼리드를 jpa로 쓸 것이라서 em을 넣고 jpa 쿼리 팩토리가 필요합니다. 이것을 주입받고 쿼리팩토리에 em을 넣습니다. 팩토리는 QueryDSL 것으로 QueryDSL는 결과적으로 jpa의 jpql을 자바 코드로 만들어주는 빌더역할을 해주는데 따라서 jpa 쿼리 팩토리가 'jpa 쿼리를 만들어주는 공장이다.'라고 하고 그 안에 em을 넣습니다.

 

> 저장, 업데이트, id로 찾기는 그냥 jpa로 사용합니다. 그 이유는 QueryDSL는 복잡한 jpql을 동적으로 만들어 주는 것이라서 jpql이 없는 경우에 사용할 필요가 없습니다.

 

-> findAll

@Override
public List<Item> findAll(ItemSearchCond cond) {
    Integer maxPrice = cond.getMaxPrice();
    String itemName = cond.getItemName();

    List<Item> result = query
            .select(QItem.item)
            .from(QItem.item)
            .where()
            .fetch();

    return result;
}

QueryDSL

를 사용합니다. 조건 2개가 있고 상품명이나 가격에 조건이 넘어오면 where를 동적으로 붙이고 and도 동적으로 붙이는 jpql이 필요합니다. 커리디는 QItem을 생성하고 query.select(item).from.where라고 하고 이것을 fetch라고 하면 리스트가 나옵니다. item의 경우에는 내부적으로 item을 가지고 있어서 이것을 씁니다. > 이것을 반환하면 레포 구현체는 끝납니다.

 

@Override
public List<Item> findAll(ItemSearchCond cond) {
    String jpql = "select i from Item i";

    Integer maxPrice = cond.getMaxPrice();
    String itemName = cond.getItemName();
    if (StringUtils.hasText(itemName) || maxPrice != null) {
        jpql += " where";
    }
    boolean andFlag = false;
    if (StringUtils.hasText(itemName)) {
        jpql += " i.itemName like concat('%',:itemName,'%')";
        andFlag = true;
    }
    if (maxPrice != null) {
        if (andFlag) {
            jpql += " and";
        }
        jpql += " i.price <= :maxPrice";
    }
    log.info("jpql={}", jpql);
    TypedQuery<Item> query = em.createQuery(jpql, Item.class);
    if (StringUtils.hasText(itemName)) {
        query.setParameter("itemName", itemName);
    }
    if (maxPrice != null) {
        query.setParameter("maxPrice", maxPrice);
    }
    return query.getResultList();
}

원래는 jpql이 필요하고 동적으로 쿼리를 더하고 쿼리를 실행했어야하는데 QueryDSL로 완전히 sql처럼 쓸 수 있습니다.

 

-> 동적쿼리

아직 동적 쿼리를 해결하지 못합니다. BooleanBuilder를 만들고 상품명 조건이 있으면 빌더에 QItem.상품명에 Like 조건을 넣습니다. 가격도 QItem의 price를 쓰고 loe(작거나 같다.)라고 하고 이 빌더를 where에 넣으면 끝납니다. 

 

@Override
public List<Item> findAll(ItemSearchCond cond) {
    Integer maxPrice = cond.getMaxPrice();
    String itemName = cond.getItemName();

    BooleanBuilder builder = new BooleanBuilder();
    if (StringUtils.hasText(itemName)) {
        builder.and(QItem.item.itemName.like("%" + itemName + "%"));
    }
    if (maxPrice != null) {
        builder.and(QItem.item.price.loe(maxPrice));
    }

    List<Item> result = query
            .select(QItem.item)
            .from(QItem.item)
            .where(builder)
            .fetch();
    return result;
}

정말 깔끔하게 코드 생산성을 이룰 수 있습니다. 동적쿼리를 자바 코드로 다 넣고 쉽게 이해할 수 있습니다.

 

- 설명

QueryDSL를 쓰려면 팩토리가 필요합니다. 팩토리는 JPQL을 만들기에 내부에 em이 필요합니다. config를 바꾸고 테스트를 하면 잘 동작합니다. 로그에 jpql이 만들어즈는 것으로 보아서 QueryDSL가 자바 코드로 jpql을 만들어줍니다.

 

-> 리팩토링

    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        Integer maxPrice = cond.getMaxPrice();
        String itemName = cond.getItemName();

        List<Item> result = query
                .select(QItem.item)
                .from(QItem.item)
                .where(likeItemName(itemName), maxPrice(maxPrice))
                .fetch();
        return result;
    }

    private BooleanExpression likeItemName(String itemName) {
        if (StringUtils.hasText(itemName)) {
            return QItem.item.itemName.like("%" + itemName + "%");
        }
        return null;
    }

    private BooleanExpression maxPrice(Integer maxPrice) {
        if (maxPrice != null) {
            return QItem.item.price.loe(maxPrice);
        }
        return null;
    }
}

더 깔끔한 방법이 있습니다. 상품명 like 조건을 메서드로 만들고 반환타입을 BooleanExpression으로 반환하고 내부에 조건을 작성합니다. 이것을 where에 넣습니다. maxPrice도 하고 where에 ','로 구분하면 and 조건이 되고 null이면 무시합니다. 이렇게 하여 조건을 진짜 sql에 where 하듯이 할 수 있습니다.

불린빌더 없이 조건을 동적으로 할 수 있습니다. 이렇게 하면 QueryDSL 자바 코드만 봐도 어떤 sql인지 바로 알 수 있고 QueryDSL의 강점인 컴파일 오류가 발생하게 할 수 있습니다. 

 

.limit()

이 사이에 limit 이런 게 다 코드에 있으니 쉽게 jpql을 작성할 수 있습니다. 이렇게 메서드로 만들면 쿼리를 다 재사용할 수도 있습니다. 이게 자바 코드로 쿼리를 짤 때 나타나는 큰 장점입니다. 선택이 아닌 필수인 기술입니다.

Comments