관리 메뉴

나구리의 개발공부기록

데이터 접근 기술 - Querydsl, Querydsl 소개(기존 방식의 문제점 / 해결), Querydsl 설정, Querydsl 적용 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 DB 2편 - 데이터 접근 활용

데이터 접근 기술 - Querydsl, Querydsl 소개(기존 방식의 문제점 / 해결), Querydsl 설정, Querydsl 적용

소소한나구리 2024. 9. 23. 09:51

  출처 : 인프런 - 스프링 DB 2편 데이터 접근 핵심 원리 (유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2


1. Querydsl 소개 - 기존 방식의 문제점

1) Query의 문제점

  • 아래의 코드는 문자를 합치게 되면 "select * from memberwhere name like ?and age between ? and ?" 처럼 문자가 합쳐지는 버그가 발생함 -> 과거에 버그를 정말 많이 내었던 부분임
  • 쿼리는 문자이므로 Type-check가 불가능하고 실행하기 전까지 작동 여부를 확인할 수가 없음
String sql =
"select * from member" +
"where name like ?" +
"and age between ? and ?"
  • 컴파일 에러 : 좋은 에러 -> IDE에서 컴파일 시점에 그 즉시 확인할 수 있는 에러(타입 에러등), 문제가 발생 되기 전에 에러를 알 수 있음
  • 런타임 에러 : 나쁜 에러 -> 애플리케이션을 띄울때 에러가 나는 경우는 그나마 낫지만, 정상 작동은 되는데 고객이 사용할 때 발생되는 에러는 가장 안좋음 (문법오류 등..)

2) QueryDSL

  • 쿼리를 Java로 type-safe하게 개발할 수 있게 지원하는 프레임워크 -> 쿼리를 Java 코드로 작성할 수 있게 도와줌
  • 주로 JPA쿼리(JPQL)에 사용함
  • SQL도 지원하긴 하는데 복잡해서 사용하진 않음

(1) Type-safe - 프로그래밍 언어나 시스템에서 타입 관련 오류를 방지하고 데이터의 일관성을 보장

  • 컴파일시 에러 체크 가능
  • 제네릭 프로그래밍 가능
  • IDE 에서 Code-assistant, 컨트롤 + 스페이스+ .(dot) 등의 기능을 활용할 수 있음

3) JPA에서 Query 방법은 크게 3가지

(1) JPQL(HQL)

  • SQL과 비슷해서 금방 익숙해지지만 type-safe가 아니며 동적 쿼리 생성이 어려움
@Test
public void jpql() {
    String query =
        "select m from Member m " +
        "where m.age between 20 and 40 " +
        " and m.name like '김%' " +
        "order by m.age desc";

    List<Member> resultList = 
        entityManager.createQuery(query, Member.class)
                     .setMaxResults(3).getResultList();
}

 

(2) Criteria API

  • 장점이 동적 쿼리를 편리하게 작성할 수 있다고 하는데 아님.. 실제로해보면 그렇지 않음.. 눈에 안들어옴
  • type-safe도 아니고 복잡하고 알아야 할게 많음
@Test
public void jpaCriteriaQuery() {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Member> cq = cb.createQuery(Member.class);
    Root<Member> root = cq.from(Member.class);

    Path<Integer> age = root.get("age");
    Predicate between = cb.between(age, 20,40);

    Path<String> path = root.get("name");
    Predicate like = cb.like(path, "김%");

    CriteriaQuery<Member> query = cq.where( cb.and(between, like) );
    query.orderBy(cb.desc(age));

    List<Member> resultList = 
        entityManager.createQuery(query).setMaxResults(3).getResultList();
}

 

(3) MetaModel Criteria API(type-safe)

  • Criteria API + MetaModel 이며, Criteria API 와 거의 동일하지만 type-safe 지원함
  • 하지만 복잡한건 매한가지임
@Test
public void jpaCriteriaQueryWithMetamodel() {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Member> cq = cb.createQuery(Member.class);
    Root<Member> member = cq.from(Member.class);

    // Metamodel 사용
    EntityType<Member> Member_ = member.getModel();

    Path<Integer> age = member.get(Member_.age);
    Path<String> name = member.get(Member_.name);

    Predicate agePredicate = cb.between(age, 20, 40);
    Predicate namePredicate = cb.like(name, "김%");

    cq.where(cb.and(agePredicate, namePredicate));
    cq.orderBy(cb.desc(age));

    TypedQuery<Member> query = entityManager.createQuery(cq);
    List<Member> resultList = query.setMaxResults(3).getResultList();
}

 

** timowest라는 핀란드 개발자가 이런 복잡한 동적쿼리 문제를 해결하기 위해 QueryDSL을 개발함


2. Querydsl 소개 - 해결

1) QueryDSL 분석

  • DSL : 도메인 + 특화 + 언어, 특정한 도메인에 초점을 맞춘 제한적인 표현력을 가진 컴퓨터 프로그래밍 언어
    • Domain : 도메인
    • Specific : 특화
    • Language : 언어
  • 쿼리 + DSL : 쿼리에 특화된 프로그래밍 언어
    • 단순하고 간결하며 유창함
    • 다양한 저장소 쿼리 기능을 통합
  • JPA, MongoDB, SQL 같은 기술들을 위해 type-safe SQL을 만드는 프레임 워크

2) QueryDSL - JPA

  • 많은 DB기술에 접근하기 위한 통합 문법을 제공하지만 JPA 쿼리(JPQL)을 typesafe하게 작성하는데 많이 사용됨
  • QueryDSL을 사용할 때는 APT(Annotation Processing Tool)를 통해 엔터티 클래스에서 Q 클래스를 생성함
  • JPA의 경우에는 @Entity가 붙은 코드를 읽어서 Q엔터티가 생성됨
  • QuesryDSL이 JPQL을 생성하고 그후 SQL도 생성해서 실행

좌) @Entity 적용 후 APT를 통한 Q클래스 생성 / 우) QueryDSL 작동 방식

  • @Entity가 붙은 @Member를 읽어서 APT가 QMember를 생성
  • JPQL을 먼저 생성하고 그후 SQL로 번역해서 실행함

@Entity를 읽어서 Q엔터티가 생성되고, 자동으로 쿼리도 만들어서 실행함(JPQL -> SQL)

  • QueryDSL - JPA 버전은 생성된 Q클래스를 JPQL을 만들고 실행해주는 빌더라고 이해하면 되는데, 결국 JPA 자체를 잘 알아야하고 JPA가 제공하는 JPQL을 잘 알아야 실무에서 사용할 수 있음

3) 장단점 및 기능

(1) 장점

  • type-safe 제공
  • 단순하고
  • 쉬움

(2) 단점

  • Q코드 생성을 위한 APT 설정이 좀 복잡한데, 한번 세팅하고 쓰면 편리함

(3) 구성 및 기능

  • Query 관련 기능
    • fetch() : 목록 조회
    • fetchOne() : 단건 조회
    • from : 데이터 소스 지정
    • innerJoin, join, leftJoin, fullJoin : 조인 처리
    • on : 조인 조건 설정
    • where (and, or, allOf, anyOf) : 조건절
    • groupBy : 그룹핑
    • having : 그룹 조건
    • orderBy (desc, asc) : 정렬
    • limit, offset, restrict(limit + offset) : 페이징 처리
  • Path, 경로
    • QMember
    • QMember.name
  • Expression, 표현식
    • member.age.eq(30): equals, age = 30
    • member.name.contains("John"): 포함, name Like '%John%'
    • name.gt(30): greater than, name > 30
  • 단순 쿼리, 동적 쿼리, 조인쿼리, 페이징, 정렬 지원

동적쿼리, 조인, 페이징, 정렬 예시

4) SpringDataJPA + Querydsl

  • SpringData 프로젝트의 약점은 조회이지만 Querydsl로 조회 기능을 보완하여 복잡한 쿼리 및 동적 쿼리가 가능해짐
  • 단순한 경우에는 SpringDataJPA를 사용하고 복잡한 경우 Querydsl을 직접 사용하면됨
  • type-safe가 지원되어 컴파일 오류로 문제의 원인을 직관적으로 볼수 있고 IDE 지원을 통해 코드 및 쿼리 작성이 매우 수월해지기 때문에 한번 써보면 안쓰기가 어려움.. (자바 코드로 IDE의 지원을 받으며 짜기 때문에 작성이 수월하고 재밌음)
  • JPQL로 해결하기 어려운 영역이 존재하는데 이부분은 JPA 기본편 강의에서 JPQL을 설명하면서 어떤 한계가 있는지 설명하고, 이런 부분에 부딪힐때는 JdbcTemplate이나 MyBatis를 사용해서 네이티브 SQL쿼리를 사용해서 해결하면 됨

3. Querydsl 설정

  • IDE, 그레이들 버전이나 환경에 따라 설정 방법이 조금씩 달라질 수 있으므로 동작이 안된다면 인터넷 검색을 활용해야 함(메뉴얼에는 잘 안나와있음)

(1) 스프링 부트 3.x 설정

  • build.gradle에 의존성 및 clean으로 Q클래스 제거하는 설정 추가

** 참고

  • 20241121 스프링 부트 수업 들으며 확인한 결과 스프링 부트 3.0.2 기준 querydsl도 스프링 부트에서 자동으로 버전관리를 해주기 때문에 버전명 없이 implementation 'com.querydsl:querydsl-jpa'로만 추가해줘도 됨
dependencies {
    //Querydsl 추가
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
//IntelliJ 설정시에만 필요하고 Gradle 설정시에는 해당 코드없어도 gradle clean 으로 제거 됨
clean {
    delete file('src/main/generated')
}

 

(2) 스프링 부트 2.x 설정

dependencies {
    //Querydsl 추가
    implementation 'com.querydsl:querydsl-jpa'
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
//IntelliJ 설정시에만 필요하고 Gradle 설정시에는 해당 코드없어도 gradle clean 으로 제거 됨
clean {
    delete file('src/main/generated')
}

2) Q 타입 생성 확인 방법

  • setting -> Build Tools -> Gradle에서 Build and run using과 Run tests using에 Gradle인지, IntelliJ 인지에 따라 설정이 다름

좌) Gradle 빌드 및 실행 / 우) IntelliJ 빌드 및 실행

 

(1) Gradle로 설정 시

  • InteliJ에서 우측 상단 코끼리모양(Gradle) 클릭 후 clean 후 compileJava 실행
    • Gradle -> Tasks -> build -> clean
    • Gradle -> Tasks -> other -> compileJava
  • 콘솔에서 사용시 gradle clean compileJava라고 입력하면 됨
  • build -> generated -> sources -> annotationProcessor -> java/main 하위에 hello.itemservice.domain.QItem이 생성됨
  • clean을 수행하면 build 폴더 자체가 삭제되므로 별도의 설정은 없어도 됨

QItem이 생성되어있는 모습, 좌) Gradle 실행 시, 우) IntelliJ 실행 시

 

(2) IntelliJ 설정시

  • 스프링 부트 3.2.x 버전부터 매개변수 이름을 파싱하는 방식이 변경되어서 가급적 gradle로 설정하는 것이 편리함
  • main() 애플리케이션 실행 혹은 상단의 Build -> Rebuild 혹은 Build -> Build Project를 실행해주면됨
  • src/main/generated 하위에 hello.itemservice.domain.QItem이 생성됨
  • IntelliJ 설정시에는 Q파일을 직접 삭제 해야하는데, build.gradle에 추가한 clean { ... } 설정으로 gradle clean 명령어 실행으로 Q파일을 삭제 할 수 있음

** 참고

  • Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋음
  • gradle 옵션을 선택하면 Q 타입은 gradle build폴더 아래에 생성되는데 대부분 gradle build 폴더를 git에 포함하지 않기 때문에 이부분은 자연스럽게 해결됨
  • InteliJ IDEA 옵션을 선택하면 src/main/generated 폴더 아래에 생성되기 때문에 해당 경로를 포함하지 않도록 gitignore로 설정 해줘야함
  • Querydsl은 이렇게 설정하는 부분이 사용하면서 조금 귀찮은데, IDE나 Querydsl의 Gradle 설정이 버전업하면서 적용 방법이 조금 달라지거나 본인의 작업 환경에 따라서 잘 동작하지 않을 수 있지만 querydsl gradle로 검색하면 대안을 금방 찾아서 적용할 수 있음(메뉴얼에는 안나와있음)

4. Querydsl 적용

1) JpaItemRepositoryV3

  • Querydsl을 사용하려면 JPAQueryFactory가 필요한데, JPAQueryFactory는 JPA 쿼리인 JPQL을 만들기 때문에 EntityManager가 필요함 -> JdbcTemplate 설정하는 것과 유사함
  • JPAQueryFactory를 스프링 빈으로 등록해서 사용해도 됨

(1) findAllOld()

  • Querydsl을 사용하는 기본적인 방법으로 동적 쿼리 문제를 해결
  • BooleanBuilder를 사용해서 if문을 사용해서 조건들을 설정하고 where에 설정한 조건을 넣어주면 됨
  • 자바 코드로 작성하기 때문에 동적 쿼리를 매우 편리하게 작성할 수 있음
package hello.itemservice.repository.jpa;

// static Import - QItem
import static hello.itemservice.domain.QItem.*;

@Repository
@Transactional
public class JpaItemRepositoryV3 implements ItemRepository {

    // JPA 사용
    private final EntityManager em;
    private final JPAQueryFactory query;

    // 주입: JPAQueryFactory에 EntityManager를 입력하여 생성
    public JpaItemRepositoryV3(EntityManager em) {
        this.em = em;
        this.query = new JPAQueryFactory(em);
    }

	// ... JPA 사용 코드와 동일

    // Querydsl 사용
    // @Override
    public List<Item> findAllOld(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        // static Import 하면 해당 코드를 생략해도 됨
//        Item item = QItem.item;

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

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

        return result;
    }
}

2) 설정 및 실행

(1) QuerydslConfig 생성

  • JPA를 사용했으므로 리포지토리 주입을 EntityManager를 사용하여 JpaItemRepositoryV3로 생성
package hello.itemservice.config;

@Configuration
@RequiredArgsConstructor
public class QuerydslConfig {

    private final EntityManager entityManager;

    @Bean
    public ItemService itemService() {
        return new ItemServiceV1(itemRepository());
    }

    @Bean
    public ItemRepository itemRepository() {
        return new JpaItemRepositoryV3(entityManager);
    }
}

 

(2) ItemServiceApplication에 Import 설정 변경

  • 생성한 QuerdydslConfig로 @Import 변경
@Import(QuerydslConfig.class)

 

(3) 실행

  • findItems 테스트가 정상 실행 되며 애플리케이션도 모두 잘 동작함

(4) 예외 변환

  • Querydsl은 별도의 스프링 예외 추상화를 지원하지 않지만 대신 @Repository가 적용해서 스프링 예외 추상화를 적용

3) JpaItemRepositoryV3 - findAll() 리펙토링

  • findAllOld의 코드에서 동적쿼리 조건들을 메서드 추출로 리펙토링하여 훨씬 깔끔하고 이해하기 쉽게 변경
  • Querydsl에서 where(A,B)에 다양한 조건들을 직접 넣게되면 AND 조건으로 처리하게 되고 where()에 null이 반환되면 해당 조건은 무시하게 됨
  • 해당 코드의 장점은 동적 쿼리 조건을 메서드 추출로 likeItemName(), maxPrice() 처럼 메서드화 했기 때문에 다른 쿼리를 작성할 때 재사용 할 수 있음 -> 자바 코드로 개발했기 때문에 쿼리 조건을 부분적으로 모듈화 할 수 있는 큰 장점
    // findAll - 리펙토링
    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        // 메서드 추출을 활용해서 간결해진 쿼리 코드
        return query
                .select(item)
                .from(item)
                .where(likeItemName(itemName), maxPrice(maxPrice))
                .fetch();
    }

    // 조건들을 메서드추출로 따로 빼서 관리
    private BooleanExpression maxPrice(Integer maxPrice) {
        if (maxPrice != null) {
            return item.price.loe(maxPrice);
        }
        return null;
    }

    private BooleanExpression likeItemName(String itemName) {
        if (StringUtils.hasText(itemName)) {
            return item.itemName.like("%" + itemName + "%");
        }
        return null;
    }
  • 리펙토링 코드도 테스트 및 애플리케이션 실행해서 테스트해보면 정상 동작함

4) 정리

  • Querydsl 덕분에 동적 쿼리를 매우 깔끔하게 이해할 수 있게 사용할 수 있게 됨
  • 쿼리 문장에 오타가 있어도 컴파일 시점에 오류를 막을 수 있게됨 
  • 메서드 추출을 통해서 코드를 재사용 할 수 있게됨(동적 쿼리 조건을 모듈화 하여 다른 비슷한 쿼리가 필요할 때 함께 사용이 가능함)
  • Querydsl을 사용해서 자바 코드로 쿼리를 작성하성하면 위 장점 외에도 최적의 쿼리결과를 만들기 위해 DTO로 편리하게 조회하는 기능(Q엔터티 생성)등의 수많은 편리한 기능을 제공함 - DTO로 조회하는 기능은 실무에서 자주 사용하는 기능
  • JPA를 사용한다면 스프링 데이터 JPA와 Querydsl은 실무의 다양한 문제를 편리하게 해결하기 위해 기본으로 선택해야 하는 기술
  • Querydsl을 잘 사용하려면 JPQL 문법을 잘 알아야 함
  • Querydsl의 자세한 강의는 실전! Qeurydsl, JPA의 자세한 강의는 JPA 기본편 강의에서 다룸