사용 이유
사용해 보고 느낀점은
- 쿼리를 java 코드로
런타임 에러를 잡을 수 있다
중복되는 쿼리를 재사용할 수 있다 - 동적 쿼리
동적 쿼리를 다뤄본 적이 없어서 몰랐는데 이번 기회로 알게 되었다.
쉽게 말해 분기에 따라 쿼리가 달라지는 경우.
1번과 비슷하다
설정
Copy codeplugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
}
group = 'study'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
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"
}
tasks.named('test') {
useJUnitPlatform()
}
clean {
delete file('src/main/generated')
}
플러그인, 의존성, clean 옵션 추가
사용법
기본적인건 생략
Projection
Copy code @Test
public void simpleProjection() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
@Test
public void tupleProjection() {
List<Tuple> result = queryFactory
.select(member.username, member.age)
.from(member)
.fetch();
for (Tuple tuple : result) {
String username = tuple.get(member.username);
Integer age = tuple.get(member.age);
System.out.println("username = " + username);
System.out.println("age = " + age);
}
}
현재 프로젝트에서 사용중인 jpql
Copy code
@Query("SELECT s.restaurantId " +
"FROM UserRestaurantScrap s " +
"WHERE s.userId = :userId " +
"ORDER BY s.createdAt DESC")
List<String> findAllRestaurantIdByUserId(String userId);
여기까진 별 감흥이 없다
DTO 변환
JPQL
Copy code @Test
public void findDtoByJPQL() {
List<MemberDto> result = em.createQuery(
"select new study.querydsl.dto.MemberDto(m.username, m.age) from Member m", MemberDto.class)
.getResultList();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
QueryDSL
Copy code@Test
public void findDtoBySetter() {
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age
))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
@Test
public void findDtoByField() {
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age
))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
@Test
public void findDtoByConstructor() {
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age
))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
@Test
public void findDtoByQueryProjection() {
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
여기서부터는 조금 편리할수도 있겠다 생각했다.
하지만 지금까지는 Repository에서는
Entity 혹은 List로 데이터를 꺼내는 경우밖에 없어서 크게 와닿지는 않았다.
하지만 프로젝트에서 DTO로 꺼내야 하는 경우가 있다면 확실히 편리할 것 같다.
그래도 아직까지는 굳이?
동적쿼리
이름, 나이가 주어지면 이름과 나이가 같은 회원을 조회
주어지지 않으면 조건에서 제외하는 쿼리
Copy code private List<Member> searchMember1(String usernameParam, Integer ageParam) {
BooleanBuilder booleanBuilder = new BooleanBuilder();
if (usernameParam != null) {
booleanBuilder.and(member.username.eq(usernameParam));
}
if (ageParam != null) {
booleanBuilder.and(member.age.eq(ageParam));
}
return queryFactory
.selectFrom(member)
.where(booleanBuilder)
.fetch();
}
Copy code private List<Member> searchMember2(String usernameParam, Integer ageParam) {
return queryFactory
.selectFrom(member)
.where(usernameEq(usernameParam), ageEq(ageParam))
.fetch();
}
private Predicate ageEq(Integer ageParam) {
if (ageParam == null) {
return null;
}
return member.age.eq(ageParam);
}
private Predicate usernameEq(String usernameParam) {
if (usernameParam == null) {
return null;
}
return member.username.eq(usernameParam);
}
여기서는 조금 이해가 되었다.
QueryDSL을 사용하지 않는다면
- findByName, findByAge, findByNameAndAge 3가지를 따로 사용해야한다
- 그리고 로직이 복잡해져서 이 쿼리를 중첩해서 사용해야한다면?
1번은 코드가 안이쁘다 정도이지만
2번은 코드를 관리하기 정말 힘들 것 같다.
현재 프로젝트에서 사용, 소감
현재 사용중인 JPQL 코드
Copy code reader.setQueryString("SELECT r FROM Restaurant r " +
"WHERE r.openDataInformation.fullAddress LIKE :address" +
" AND r.crawlComplete = false" +
" AND r.restaurantId NOT IN (SELECT f.recordDataId FROM FailedRecord f)");
reader.setParameterValues(Map.of("address", "%마포구%"));
Copy code @Query("SELECT s.restaurantId " +
"FROM UserRestaurantScrap s " +
"WHERE s.userId = :userId " +
"ORDER BY s.createdAt DESC")
List<String> findAllRestaurantIdByUserId(String userId);
Copy code @Query("SELECT r " +
"FROM RestaurantNaverReviewFeatureCount r " +
"JOIN FETCH r.naverReviewFeature " +
"WHERE r.restaurantId = :restaurantId " +
"ORDER BY r.reviewCount DESC " +
"LIMIT 2")
List<RestaurantNaverReviewFeatureCount> findTop2ByRestaurantIdOrderByReviewCountDesc(String restaurantId);
복잡한건 이정도이다.
결론
코드 유지보수 측면에서 좋은것 보다도
쿼리 오류를 런타임에러가 아닌 컴파일 에러로 잡을 수 있는게 가장 큰 장점인 것 같다.
https://github.com/team2-yumst/yumst/issues/31
현재 진행중인 프로젝트에서도 쿼리 관련 버그 수정을 한 적이 있는데
이런 경우 없이 안심? 할수 있는 점이 좋은 것 같다.
내 생각에 현재는 필요 없다.
하지만 쿼리 최적화를 계획하고 있는데,
그때 필요하다면 사용하기로.
'SUMMARY > Spring' 카테고리의 다른 글
[Spring Boot] 모니터링 (0) | 2025.06.06 |
---|---|
[Spring] Servlet, Spring MVC (0) | 2025.06.06 |