Querydsl - 시작하기
Spring Data JPA를 사용하는 기업들이 많아지면서 그와 함께 Querydsl을 사용하는 기업들도 자연스럽게 증가했습니다. 오늘은 Querydsl을 사용하기 위한 프로젝트 환경설정 방법과 잘 적용이 됐는지 동작을 확인해보도록 하겠습니다.
먼저, Querydsl에 대해 소개해드리면, JPA가 사용되기 이전에는 MyBatis를 많이 사용했습니다. 그러나 JPA가 등장하면서 새로운 프로젝트를 시작하는 회사에서는 JPA + Querydsl을 기본으로 가져가는 구조로 자리가 잡혔습니다. 그러나 JPA는 MyBatis 대비 동적 쿼리를 구현하는 데 있어서 한계가 있습니다. 그리하여 Querydsl은 JPA로 구현하기 힘든 동적 쿼리를 보완해주는 기술입니다. 또한 MyBatis의 경우에는 쿼리를 짠 후에는 직접 어플리케이션을 실행하고 특정 쿼리가 수행될 때 에러가 있는지 없는지를 알 수 있는 런타임 에러가 발생하지만, Querydsl의 경우는 자바로 직접 쿼리를 작성하기 때문에 컴파일단에서 미리 오류를 확인하여 수정할 수 있는 컴파일 에러가 발생하도록 하는 장점도 있습니다.
프로젝트 버전
- 개발 도구: IntelliJ Ultimate
- Spring Boot: 2.5.7
- Java 11
- h2 Database
- JUnit 5
- Spring Web, Spring Data JPA, Lombok
먼저 Spring Data JPA를 사용해보도록 하겠습니다. 간단하게 Member Entity를 만들고, Repository 생성하여 데이터를 Insert 해보도록 하겠습니다.
Member
@Getter
@Entity
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@Column(name = "member_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String name;
@Builder
private Member(String email, String name) {
this.email = email;
this.name = name;
}
}
MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long> {
}
MemberRepositoryTest
@SpringBootTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Test
public void insertMemberTest() {
Member member = Member.builder()
.email("wangtak@gmail.com")
.name("왕탁이")
.build();
Member savedMember = memberRepository.save(member);
// assertj Assertions static import
assertThat(savedMember.getEmail()).isEqualTo("wangtak@gmail.com");
assertThat(savedMember.getName()).isEqualTo("왕탁이");
}
}
Member Table
JPA가 잘 동작하게 끔 설정을 마치고 Querydsl을 적용해보도록 하겠습니다. Querydsl은 Spring에서 관리하는 라이브러리가 아니기 때문에 start.spring.io에서 추가할 수는 없고 직접 추가해야 합니다. build.gradle에 다음과 같이 추가해주시면 됩니다.
build.gradle
plugins {
id 'org.springframework.boot' version '2.5.7'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
// querydsl 추가
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
id 'java'
}
group = 'com.wangtak'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
// querydsl 추가
implementation 'com.querydsl:querydsl-jpa'
runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
// querydsl 세팅
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
build.gradle에 위와 같이 추가해주시고 Reload All Gradle Projects를 해주세요. (build.gradle 우측 상단에 코끼리 모양 버튼 클릭.) 혹은 Help -> Find Action -> 검색에 Reload All ... 치시고 클릭
그 이후엔 아래 그림과 같이 진행합니다.
IntelliJ 기준으로 우측에 Gradle -> 프로젝트의 이름 -> Tasks -> other -> compileQuerydsl 을 클릭해주세요. 그러면 컴파일이 되고 다음과 같은 위치(build -> generated -> querydsl 이하 디렉토리)에 새로운 파일이 생성됩니다.
위 사진과 글 처럼 5번의 compileQuerydsl을 실행하였는데 Unable to load class 'com.mysema.codegen.model.Type' 에러가 발생한다면 build.gradle을 다음 글처럼 바꿔서 설정을 해주시기 바
랍니다. (스프링 부트의 버전이 2.6 이상이라면 다음 글로 해결하실 수 있을겁니다.)
그러면 이제 저 QMember를 사용하여 저장된 Member Data를 조회해보도록 하겠습니다. 먼저 실무에서 Querydsl을 사용하는 방법은 Custom Repository 인터페이스를 만들어서 인터페이스의 구현체를 만들고 그 구현체에서 순수 JPA 혹은 Querydsl을 사용합니다. JPA Repository에서는 Custom Repository Interface를 상속하여 사용합니다.
MemberRepositoryCustom
// Custom Repository Interface
public interface MemberRepositoryCustom {
List<Member> querydslFindAll();
}
MemberRepositoryCustomImpl
// Custom Repository Interface 구현체
// 이쪽에서 순수 JPA or Querydsl을 사용해줌.
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
// JPAQueryFactory는 Bean으로 등록해서 사용해도 됨.
// 다른 식으로 사용하는 방법을 나중에 포스팅
// 또한 이게 가능한 이유는 생성자가 하나일 때는 알아서 @Autowired가 붙음
public MemberRepositoryCustomImpl(EntityManager em) {
jpaQueryFactory = new JPAQueryFactory(em);
}
@Override
public List<Member> querydslFindAll() {
return jpaQueryFactory
.selectFrom(QMember.member)
//.selectFrom(member) // QMember를 static import하면 다음과 같이 축약 가능
.fetch();
}
}
MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
MemberRepositoryTest
@Test
public void querydslFindAllTest() {
List<Member> members = memberRepository.querydslFindAll();
members.iterator().forEachRemaining(member -> {
System.out.println("member = " + member);
});
}
여기서 MemberRepositoryCustom, MemberRepositoryCustomImpl에 스프링 빈으로 등록하겠다는 어노테이션 없이 자연스럽게 사용되는 것을 알 수 있습니다. 이것은 Spring Data 내부에서 사용자 정의 인터페이스 명 + Impl과 같이 사용하게 되면 알아서 스프링 빈으로 등록해줍니다. [사용자 정의 레파지토리]
일종의 관례고 규칙이기 때문에 "인터페이스 명 + Impl"은 지키는 것이 좋습니다.
이렇게 Querydsl을 적용해보고 확인해봤습니다. 순수 JPA, Spring Data JPA를 처음 접하고 사용했을 때 정말 놀라웠고 Querydsl 또한 정말 박수가 나올 만큼 엄청난 기술이라고 생각합니다. 다음에는 더 자세한 Querydsl의 내용을 정리해보도록 하겠습니다.