본문 바로가기
Querydsl

Querydsl 간단하게 알아보기

by codeyaki 2023. 8. 5.
반응형

업무에서 Querydsl을 사용하게 될 것 같아서 간단하게 알아보는 시간을 가져 보았다.

Querydsl를 짧게 표현하자면 SQL, JPQL등 쿼리를 코드로 작성하게 해주는 빌더 오픈소스 프레임워크이다.

소개

배경

  • Querydsl을 사용하기 이전에는 query를 문자열로 작성해야 했다. JPQL을 생각해 보면 아래와 같이 작성하고 있었다.
  • @query("select a from Test a") Optional<Test> findAll();
  • 이와 같이 작성하면 쿼리를 문자열로 작성하다보니 실수할 수 있고 또 문제가 컴파일 때 발생하지 않고 실행 시에만 문제를 찾을 수 있다는 단점이 있었다.
  • 이를 해결하고자 Querydsl은 자바코드로 쿼리를 작성할 수 있게 해주어 개발자의 실수를 줄일고 컴파일 타임에 오류를 찾을 수 있도록 하여 문제를 해결하였다.
  • HQL 뿐만아니라 JPA, JDO, JDBC, Lucene, Hibernate Search, MongoDB, Collections 및 RDFBean 다양한 곳에서 지원한다.

원칙

  • 타입 안전성 (Type safety)
    • 핵심 원칙으로 쿼리는 도메인 유형의 속성을 반영하는 생성된 쿼리 유형을 기반으로 ㄱ구성된다. 또한 메서드 호출은 완전히 형식이 안전한 방식으로 구성된다.
  • 일관성
    • 쿼리 paths 및 operations은 모든 구현에서 동일하며 쿼리 인터페이스에도 공통 기본 인터페이스를 가지고 있다.

설정하기

Querydsl을 사용하기 위해서는 기본적인 설정이 필요하다. 빌드 툴에 따라서 방법이 다르다!

메이븐

메이븐에서는 pm.xml에 두가지 작업을 해주어야 한다.

  1. 종속성 추가
    • querydsl-apt모듈은 Querydsl의 Annotation Porcessor Tool(APT)이다.
      • 개발자가 작성한 Querydsl 쿼리 클래스를 분석하여 쿼리를 생성해 주는 역할
      • 해당 쿼리는 querydsl-core 모듈에서 사용된다.
    • querydsl-jpa모듈은 JPA와 함께 사용하기 위한 Querydsl 모듈이다.
      • 해당 모듈에는 querydsl-core 모듈이 내포되어 있다.
      • 쿼리를 작성하고 실행하는데 필요한 기능을 제공한다.
  2. <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>${querydsl.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>${querydsl.version}</version> </dependency>
  3. apt 플러그인 설정
    • JPAAnnotationProcessor는 javax.persistence.Entity 주석이 달린 도메인 유형을 찾아 쿼리타입을 생성해준다. (만약 Hibernate 어노테이션을 사용한다면, APT프로세서를 com.querydsl.apt.hibernate.HibernateAnnotationProcessor 으로 변경해주어야 한다.)
    • mvn clean install을 진행하면, target/generated-sources/java 디렉터리에 QClass(Query 타입)이 생성된다. 만약 이클립스를 사용할 경우, mvn eclipse:eclipse 를 실행하면 target/generated-sources/java 디렉터리가 소스 폴더에 추가된다.
    • 생성된 Query 타입을 이용하면 JPA 쿼리 인스턴스와 쿼리 도메인 모델 인스턴스를 생성할 수 있다.
  4. <project> <build> <plugins> ... <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> </plugin> ... </plugins> </build> </project>

그래들

그래들은 공식문서에 나와있지 않아 여러 블로그들을 참고하였다.

build.gradle에 아래와 같이 종속성들을 추가해 주면 된다.

dependencies {

	//querydsl
	implementation "com.querydsl:querydsl-jpa"
	// implementation "com.querydsl:querydsl-core"
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" // querydsl JPAAnnotationProcessor 사용 지정
	annotationProcessor "jakarta.annotation:jakarta.annotation-api" // java.lang.NoClassDefFoundError (javax.annotation.Generated) 대응 코드
	annotationProcessor "jakarta.persistence:jakarta.persistence-api" // java.lang.NoClassDefFoundError (javax.annotation.Entity) 대응 코드
}

def generated = "$buildDir/generated/querydsl"

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
	options.getGeneratedSourceOutputDirectory().set(file(generated))
}

// java source set 에 querydsl QClass 위치 추가
sourceSets {	
	main.java.srcDirs += [ generated ]
}

// gradle clean 시에 QClass 디렉터리 삭제
clean {
	delete file(generated)
}
  • 메이븐과 마찬가지로 querydsl-jpa와 querydsl-apt를 추가해주면 된다.
  • gradle clean complieJava를 진행하면 지정해 둔 경로에 QClass가 생성된다.
  • 이때 querydsl-apt는 APT이기 때문에 annotationProcessor로 등록해 주면 된다.
annotationProcessor "jakarta.annotation:jakarta.annotation-api" // java.lang.NoClassDefFoundError (javax.annotation.Generated) 대응 코드
annotationProcessor "jakarta.persistence:jakarta.persistence-api" // java.lang.NoClassDefFoundError (javax.annotation.Entity) 대응 코드
  • 이 두 코드는 Q파일을 찾지 못해 발생하는 오류인 java.lang.NoClassDefFoundError(javax.annotation.Entity / javax.annotation.Generated)에러에 대응하기 위한 코드이다.

def generated = "$buildDir/generated/querydsl"

  • 빌듯이 querydsl Q클래스들을 위치시킬 경로

JavaCompile

compileJava {
    options.compilerArgs << '-Aquerydsl.generatedAnnotationClass=javax.annotation.Generated'
}
  • 해당 코드는 Q파일 내 Generated를 import 할 때 Java 9에 있는 javax.annotation.processing.Generated로만 import가 되게 된다. Java 9 버전 이하 버전일 경우 넣어줘야 하는 것으로 보인다.

soruceSets

// java source set 에 querydsl QClass 위치 추가
sourceSets {	
	main.java.srcDirs += [ generated ]
}
  • 빌듯이 QClass 소스도 함께 build 하기 위해서 sourceSets 추가

tasks.withType

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
	options.getGeneratedSourceOutputDirectory().set(file(generated))
}

clean

// gradle clean 시에 QClass 디렉터리 삭제
clean {
	delete file(generated)
}
  • build clean시에 생성되었던 QClass를 모두 삭제시키는 코드이다.

일단 사용해 보기

Querydsl은 생각보다 간단하게 사용할 수 있다.

  1. 퀴리 타입 생성하기 (QClass 이용)
    • 아래 두 가지 방법 중 한 가지를 택하면 된다.
      • QEntity entity = QEntity.entity;
      • QEntity entity = new QEntity(”myTestEntity”);
  2. JPAQueryFactory 생성하기
    • 생성하기 위해서는 EntityManager를 필요로 한다.
  3. 쿼리 사용하기
@SpringBootTest
public class EntityTest {
	@Autowired
	EntityManager em;

	QEntity entity = QEntity.entity;
	
	@Test
	void selectTest() {
	    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
	    Entity findEntity= queryFactory
	            .selectFrom(entity)
	            .where(entity .id.eq("test"))
	            .fetchOne();
	
	    assertThat(findFactory.getId()).isEqualTo("test");
	}
}

DI 주입하기

Querydsl을 사용하는 모든 클래스에서 JPAQueryFactory를 생성하기보다 DI를 통해 간편하게 사용하도록 만들 수 있다.

package com.pno.web;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Configuration
public class QuerydslConfig {
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}
}

Repository 만들기

기존에 사용하던 JpaRepository의 기능들 또한 모두 사용해야 하기에 Spring data의 커스텀 레파지토리 만들기를 사용하여 custom Repository를 추가하여 준다. (https://docs.spring.io/spring-data/jpa/docs/2.1.3.RELEASE/reference/html/#repositories.custom-implementations)

이러한 패턴으로 생성하게 되면 사용하는 useRepository에서 모두 사용할 수 있게 된다.

  • 커스텀 레파지토리는 interface로 Querydsl을 사용하여 구현하고자 하는 메서드들을 작성하면 된다.
  • 이후 커스텀 레파지토리를 구현하여 실제 querydsl을 사용하여 쿼리를 구현한다. (이때 반드시 커스텀 레파지토리를 구현하는 클래스에는 Impl을 붙여주어야 한다.)
  • 또한 구현 클래스에는 JPAQueryFactory와 사용하고자 하는 QClass가 필요하다.
public interface TestRepositoryCustom {

    Entity findEntityOnByEntityId(String entityId);
}
import com.example.web.domain.entity;
import com.example.web.domain.QEntity;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
RequiredArgsConstructor
public class TestRepositoryCustomImpl implements TestRepositoryCustom {

    // 반드시 필요
    private final JPAQueryFactory jpaQueryFactory;
    QEntity entity = QEntity.entity;

    @Override
    public Entity findFactoryOnByFactoryId(String entityId) {
        return jpaQueryFactory
                .select(entity)
                .from(entity)
                .where(entity.id.eq(entityId))
                .fetchOne();
    }
}

spring data에서 제공하는 QuerydslRepositorySupport 클래스를 이용하는 경우도 있는데 이경우는 pageable도 제공하는 등 다양한 추가 기능들이 있다. 만약 사용하고 싶다면 impl에서 추가적으로 QuerydslRepositorySupport를 상속받아서 생성자에 super(Entity);를 추가해주기만 하면 된다.

import com.example.web.domain.entity;
import com.example.web.domain.QEntity;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;

public class TestRepositoryCustomImpl extends QuerydslRepositorySupport implements TestRepositoryCustom {

    // 반드시 필요한 곳
    private final JPAQueryFactory jpaQueryFactory;
    QEntity entity = QEntity.entity;

    @Autowired
    public FactoryRepositoryCustomImpl(JPAQueryFactory jpaQueryFactory) {
        super(Factory.class);
        this.jpaQueryFactory = jpaQueryFactory;
    }

    @Override
    public Entity findFactoryOnByFactoryId(String entityId) {
        return jpaQueryFactory
                .select(entity)
                .from(entity)
                .where(entity.id.eq(entityId))
                .fetchOne();
    }
}
반응형