SpringBoot

QueryDSL 이란?

똑똑한망치 2024. 7. 17. 21:03
728x90
반응형

QueryDSL 이란

SQL, JPQL 등을 코드로 작성할 수 있도록 해주는 빌더 오픈소스 프레임워크이다.

사실, QueryDSL 이 JPA에서만 사용하는 프레임워크로만 알 수도 있지만 공식 사이트를 보면 JPA 뿐만 아니라 SQL, MongoDB, Lucenece 등 다양한 언어에 대해서 서비스를 제공한다.

 

QueryDSL JPA

QueryDSL JPA 는

  • SQL, JPQL 을 코드로 작성할 수 있도록 해주는 빌더 API 이고,
  • Entity 클래스와 매핑되는 QClass 라는 객체를 사용해서 쿼리를 실행한다.

QClass 란?

QueryDSL은 컴파일 단계에서 엔티티를 기반으로 QClass를 생성하는데 JPAAnnotationProcessor 가 컴파일 시점에 작동해서 @Entity 등의 어노테이션을 찾아 해당 파일들을 분석해서 QClass를 만든다.

QClass 는 Entity와 형태가 똑같은 Static Class 이다.

QueryDSL은 쿼리를 작성할 때 QClass 를 기반으로 쿼리를 실행한다.

 

JPQL 이란?

JPA에서 지원하는 다양한 쿼리 방법 중 가장 단순한 조회 방법으로, SQL의 경우에는 DB 테이블을 대상으로 쿼리를 질의하지만, JPQL은 엔티티 객체를 대상으로 쿼리를 질의한다.

 

 

QueryDSL JPA를 사용해야 하는 이유

● JPQL은 쿼리를 여전히 문자열로 입력하기 때문에 오타가 발생하거나 관리하기 어렵다.

➡️ QueryDSL은 쿼리를 문자열로서가 아니라 코드를 통해서 작성하기 때문에 오타가 날 확률이 적어지고, 객체 지향적으로 개발할 수 있다.

 

● JPQL은 컴파일 단계에서 오류를 확인할 수 없고, 런타임 시 해당 쿼리가 실행되어야지만 오류를 확인할 수 있다.

➡️ QueryDSL은 코드로써 작성하기 때문에 컴파일 단계에서도 오류를 빠르게 발견할 수 있다.

 

예를 들어 

회원(member)과 포인트(point)를 조인해서 가져와야 할 때

JPQL 의 경우에는

String jpql = "select * from Member m join Point p on p.member_id = m.id"
List<Member> result = em.createQuery(jpql, Member.class).getResultList();

 

이런식으로 쿼리를 작성해야 하지만,

 

QueryDSL을 사용했을 때에는

return jpaQueryFactory
.from(member)
.join(member.point, point)
.fetch();

이런식으로 코드를 사용해서 나타낼 수 있다.

오타가 나더라도 컴파일 단계에서 오류를 확인할 수 있고, 코드로서 작성하기 때문에 더욱 객체 지향적으로 개발할 수 있다.

 

build.gradle 에 QueryDSL 세팅하기

dependencies {
	... 
	implementation 'com.querydsl:querydsl-jpa:jakarta'
	implementation 'com.querydsl:querydsl-apt'

	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
	annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
	annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
}

def querydslSrcDir = 'src/main/generated'
sourceSets {
  main {
    java {
      srcDirs += [ querydslSrcDir ]
    }
  }
}

compileJava {
    options.compilerArgs << '-Aquerydsl.generatedAnnotationClass=javax.annotation.Generated'
}

tasks.withType(JavaCompile) {
	options.generatedSourceOutputDirectory = file(querydslSrcDir)
}

clean {
  delete file(querydslSrcDir)
}

 

dependencies

implementation 'com.querydsl:querydsl-jpa:jakarta'

➡️ QueryDSL을 사용하기 위한 라이브러리

➡️ QuerydslRepositorySupport, QuerydslPredicateExecutor 등 QueryDSL 내부 클래스는 사용 가능하지만, 실제로 쿼리를 위해 사용되는 QClass는 생성되지 않는다.

 

implementation 'com.querydsl:querydsl-apt'

➡️ QClass를 생성하기 위한 라이브러리

 

annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"

➡️ QueryDSL JPa의 경우에는 QClass를 활용해서 동작하는데 @Entity 어노테이션을 선언한 클래스를 탐색하고, QClass를 생성한다.

 

QClass를 생성하는 방법에는 크게 두 가지로 나뉜다.

1. com.ewerk.gradle.plugins.querydsl

-> 2018년 이후 업데이트 된 적이 없다.

2. annotationProcessor

 

이번의 경우에는 2번을 선택하였다.

 

annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

➡️ 이클립스 재단으로 자바가 이관되면서 java -> jakarta 로 이름이 변경되었다.

➡️ Q파일을 찾지 못해서 발생하는 오류인 java.lang.NoClassDefFoundError (javax.annotation.Entity / javax.annotation.Generated) 에러 발생 대응을 위한 코드 이다.

 

sourceSets

def querydslSrcDir = 'src/main/generated'
sourceSets {
  main {
    java {
      srcDirs += [ querydslSrcDir ]
    }
  }
}

 

gradle build 시 QClass 소스도 함께 build 하기 위해서 sourceSets에 해당 위치를 추가해준다.

 

compileJava

compileJava {
    options.compilerArgs << '-Aquerydsl.generatedAnnotationClass=javax.annotation.Generated'
}


해당 내용을 명시해주지 않으면 Q 파일 내 Generated 를 import 할 때 자바 9 에만 있는 import javax.annotation.processing.Generated 로 import 해준다.

그렇기 때문에 다른 버전에서도 사용할 수 있도록 java.annotation.Generated 로 import 하도록 설정하는 코드이다.

 

task.withType

tasks.withType(JavaCompile) {
	options.generatedSourceOutputDirectory = file(querydslSrcDir)
}

➡️ annotationProcessorGeneratedSourcesDirectory 를 사용할 수도 있는데, Gradle 공식 문서에 해당 기능은 deprecated 되었고, 추후 major version 에선 삭제할 것이라고 되어있기 때문에 generatedSourceOutputDirectory 를 사용한다.

➡️ generatedSourceOutputDirectory 는 annotation processors에서 생성한 소스 파일을 저장할 디렉토리를 지정해준다.

➡️ 이 코드를 통해 위에서 선언한 querydslSrcDir 변수의 src/main/generated 에다가 annotation processors가 만든 QClass들을 저장해준다.

 

clean

clean {
	// clean 실행 시 생성된 QClass 삭제
	delete file(querydslSrcDir)
}

➡️ build clean 시에 생성되었던 QClass를 모두 삭제한다.

 

실행하기

  • 만들어진 QClass 삭제하기 : Gradle > build > clean
  • QClass 만들기 : Gradle > build > compileJava

src > main > generated 내에 QClass 가 만들어졌으면 queryDSL 설정이 정상적으로 된 것이다.

 

QueryDSL 사용하기

(1) QuerydslConfig 파일 설정하기

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

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

 

@PersistenceContext

  • EntityManager를 빈으로 주입할 때 사용하는 어노테이션
  • 빈으로 주입받을 때 EntityManager의 경우 @Autowired가 아니라 해당 어노테이션으로 주입

JPAQueryFactory를 Bean으로 등록해서 프로젝트 전역에서 사용할 수 있도록 한다.

 

(2) QueryDSL 사용 Repositorty 구조 잡기

Spring에서 QueryDSL을 사용하는 방법에는 총 3가지 방법이 있다. (Spring Data Jpa Custom Repository / QueryRepositorySupport / JPAQueryFactory) 이 있다.

 

1. Spring Data JPA Custom Repository 사용

Spring에서 QueryDSL과 JPARepository를 함께 사용하려면 각 역할 별 파일을 두 개 만들어주거나, 하나의 파일이 두 개의 의존성을 가져야 한다. 이러한 경우 Spring Data JPA 에서 제공하는 ⭐Spring Data Custom Repository ⭐ 를 사용하면 된다.

해당 방식을 통해서 CustomRepository를 JPARepository 상속 클래스에서 함께 상속 받아서 사용할 수 있다.

 

구현된 Repository 구조는 아래와 같다.

 

Repository(interface) 가 JpaRepository(interface), CustomRepository(interface)를 다중 상속 받고

➡️ CustomRepository 인터페이스에 선언되어 있는 메서드에 대한 구현은 RepositoryImpl 에서 한다.

➡️ 그리고 사용자는 Repository 인터페이스를 DI 받아서 사용한다.

 

❓RepositoryImpl 은 Repository를 직접적으로 구현하지 않는데 어떻게 Repository에서 사용가능할까 ❓

💡 사용자 정의 구현 클래스인 경우 JPA 가 파일명이 repository interface 이름 + Impl 인 클래스를 찾아서 interface에 JpaRepository를 Injection 할 때에  Impl 객체를 삽입해주기 때문에 사용할 수 있다,

 

따라서 반드시 구현체 파일명이 interface 명 + Impl 이여야 한다.

 

위와 같은 방식의 경우 총 3개의 repository 관련 파일이 만들어 진다.

 

첫번째, Repository 파일 (interface)

JpaRepository 와 Custom interface를 상속한 파일 ⭐ 사용자는 해당 파일을 DI 받아서 사용한다.⭐

public interface ContentRepository extends JpaRepository<Content, Long>, ContentRepositoryCustom {
}

 

두번쨰, RepositoryCustom 파일 (interface)

QueryDSL로 커스텀해서 사용할 메서드 선언하는 파일

public interface ContentRepositoryCustom {
}

 

세번째, RepositoryImpl 파일 (class)

RepositoryCustom interface 에 선언한 메서드 구현하는 파일

@RequiredArgsConstructor
public class ContentRepositoryImpl implements ContentRepositoryCustom {
  private final JPAQueryFactory queryFactory;
}

 

단점 : QueryDSL을 사용하는 경우 만들어야 하는 파일이 엄청 많아지고, 그에 따른 관리 포인트가 늘어난다.

 

2. QueryRepositorySupport 상속하기

첫번째 방식처럼 여러 파일을 만들어서 상속받는 방식이 아니라 QuerydslRepositorySupport만 상속하여 구조를 단순화 하는 방식이다.

@Repository 
public class ContentRepositorySupport extends QuerydslRepositorySupport {
  private final JPAQueryFactory queryFactory;
  
  public ContentRepositorySupport(JPAQueryFactory queryFactory) {
    super(Content.class);
    this.queryFactory = queryFactory;
  }
}

 

단점

  • 매번 Support를 상속 받고, super 생성자에 Entity를 등록해줘야 한다.
  • Entity를 save, delete 하는 repository와 query하는 repository가 나뉘어 진다.

 

3. JPAQueryFactory 사용하기

JPAQueryFactory만 있으면 QueryDSL 기능을 모두 사용할 수 있다.

 

2번째 방식처럼 super와 생성자를 사용하지 않아도 된다. 사용하고자 하는 QClass를 import 해서 사용하는 방식

@RequiredArgsConstructor
@Repository
public class ContentQueryRepository {
  private final JPAQueryFactory queryFactory;
  
  public List<Content> findByTitle(String title) {
    return queryFactory
              .selectFrom(content)
              .where(content.title.eq(title))
              .fetch();
  }
}

 

단점

  • Entity 를 save, delete 하는 repository 와 query 하는 Repository 가 나뉘어 진다.
반응형

'SpringBoot' 카테고리의 다른 글

Spring WebSocket  (0) 2024.07.16
JPA Entity에 @Setter를 지양하는 이유  (0) 2024.06.29
SpringSecurity Bcrypt 를 이용한 비밀번호 암호화  (0) 2024.06.26
@Controller와 @RestController 차이점  (0) 2024.06.25
RedirectAttributes  (0) 2024.05.02