build.gradle 파일 수정하여 Querydsl 의존성 추가해야 함plugins {
id 'java'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'org.example'
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-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// bcrypt
implementation 'at.favre.lib:bcrypt:0.10.2'
// jwt
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
// QueryDSL
implementation 'com.querydsl:querydsl-jpa:5.1.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()
}

JPAQueryFactory를 빈으로 등록하여 프로젝트 전반에서 사용할 수 있도록 설정해야 함package org.example.expert.config;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QueryDSLConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
Todo 엔티티를 작성해야 함package org.example.expert.domain.todo.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.example.expert.domain.comment.entity.Comment;
import org.example.expert.domain.common.entity.Timestamped;
import org.example.expert.domain.manager.entity.Manager;
import org.example.expert.domain.user.entity.User;
import java.util.ArrayList;
import java.util.List;
@Getter
@Entity
@NoArgsConstructor
@Table(name = "todos")
public class Todo extends Timestamped {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String contents;
private String weather;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@OneToMany(mappedBy = "todo", cascade = CascadeType.REMOVE)
private List<Comment> comments = new ArrayList<>();
// 매니저 객체를 영속성 컨텍스트에서 관리하기위해 CascadeType.PERSIST 설정
// Todo 를 save 할 시, managers도 같이 Persist 됨.
@OneToMany(mappedBy = "todo", cascade = CascadeType.PERSIST)
private List<Manager> managers = new ArrayList<>();
public Todo(String title, String contents, String weather, User user) {
this.title = title;
this.contents = contents;
this.weather = weather;
this.user = user;
this.managers.add(new Manager(user, this));
}
}
./gradlew build 실행하면 QueryDLS이 QTodo 클래스를 자동으로 생성해줌 (/build/Generated 에서 확인)
TodoRepository 인터페이스 작성하여 JPA와 QueryDSL을 함께 사용할 수 있도록 해야 함package org.example.expert.domain.todo.repository;
import org.example.expert.domain.todo.entity.Todo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TodoRepository extends JpaRepository<Todo, Long>, TodoRepositoryQuery {
}
package org.example.expert.domain.todo.repository;
import org.example.expert.domain.todo.dto.response.TodoResponse;
import org.example.expert.domain.todo.entity.Todo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.time.LocalDateTime;
public interface TodoRepositoryQuery {
Page<TodoResponse> findTodosByWeatherAndModifiedAtWithPages(String weather, LocalDateTime startTime, LocalDateTime endTime, Pageable pageable);
}
package org.example.expert.domain.todo.repository;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.example.expert.domain.todo.dto.response.TodoResponse;
import org.example.expert.domain.todo.entity.Todo;
import org.example.expert.domain.user.dto.response.UserResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import java.time.LocalDateTime;
import java.util.List;
import static org.example.expert.domain.todo.entity.QTodo.todo;
import static org.example.expert.domain.user.entity.QUser.user;
@RequiredArgsConstructor
public class TodoRepositoryQueryImpl implements TodoRepositoryQuery {
private final JPAQueryFactory jpaQueryFactory;
@Override
public Page<TodoResponse> findTodosByWeatherAndModifiedAtWithPages(String weather, LocalDateTime startTime, LocalDateTime endTime, Pageable pageable) {
List<TodoResponse> result = jpaQueryFactory
.select(
Projections.constructor(
TodoResponse.class,
todo.id,
todo.title,
todo.contents,
todo.weather,
Projections.constructor(UserResponse.class, user.id, user.email),
todo.createdAt,
todo.modifiedAt
)
)
.from(todo)
.leftJoin(todo.user, user)
.where(
weather != null ? todo.weather.eq(weather) : null,
startTime != null ? todo.modifiedAt.goe(startTime) : null,
endTime != null ? todo.modifiedAt.loe(endTime) : null
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Long total = jpaQueryFactory
.select(todo.count())
.from(todo)
.where(
weather != null ? todo.weather.eq(weather) : null,
startTime != null ? todo.modifiedAt.goe(startTime) : null,
endTime != null ? todo.modifiedAt.loe(endTime) : null
)
.fetchOne();
return new PageImpl<>(result, pageable, total != null ? total : 0L);
}
}
package org.example.expert.domain.todo.service;
import lombok.RequiredArgsConstructor;
import org.example.expert.domain.todo.dto.response.TodoResponse;
import org.example.expert.domain.todo.entity.Todo;
import org.example.expert.domain.todo.repository.TodoRepository;
import org.example.expert.domain.user.dto.response.UserResponse;
import org.example.expert.domain.user.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
public class TodoService {
private final TodoRepository todoRepository;
@Transactional(readOnly = true) // 조회는 readOnry = true 설정
public Page<TodoResponse> getTodos(int page, int size, String weather, LocalDateTime startTime, LocalDateTime endTime) {
Pageable pageable = PageRequest.of(page - 1, size);
return todoRepository.findTodosByWeatherAndModifiedAtWithPages(weather, startTime, endTime, pageable);
}
}
Querydsl의 장점:
Querydsl의 단점:
다른 기술과의 비교: