
현재 진행 중인 프로젝트에서는 JPA를 기반으로 데이터 처리를 하고 있는데 기본 CRUD만으로는 복잡한 조회 쿼리를 처리하기 어려워 QueryDSL을 함께 사용하고 있다.
QueryDSL이 뭔지 정확히 알고 싶어서 정리하기!
✅ QueryDSL ?
QueryDSL은 HQL(하이버네이트 쿼리 언어 : Hibernate Query Language) 기반의 쿼리를 타입에 안전하게(type-safe) 생성하고 관리할 수 있도록 도와주는 프레임워크다.
JPQL, SQL, JPA, MongoDB 등의 쿼리를 자바 코드로 표현할 수 있게 해주며 정적 타입을 활용해 SQL과 유사한 쿼리를 Java 코드 수준에서 생성할 수 있도록 지원해준다.
JPA는 기본적으로 단순한 CRUD 작업에는 적합하지만 복잡한 조건 쿼리나 동적 쿼리를 구현하는 데에는 한계가 있다.
이러한 한계를 보완하기 위해 QueryDSL을 사용하면 자바 코드로 쿼리를 작성하면서도 조건 조합, 동적 필터링, 다중 조인 등을 타입 안정성과 함께 구현할 수 있다.
또한 QueryDSL은 자바 컴파일 시점에 쿼리 오류를 잡아낼 수 있기 때문에 런타임 오류 발생을 줄이고 유지보수성을 크게 향상시킬 수 있다.
☑️ JPA repository / QueryDSL
이름이 "홍길동"이고 나이가 "20"인 사용자 리스트를 조회하려고 한다.
JPA Repository 방식이면
//UserRepository
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByNameAndAge(String name, int age);
}
List<User> users = userRepository.findByNameAndAge("홍길동", 20);
이렇게 간단하게 만들 수 있다.
QueryDSL인 경우
//UserRepositorySupport
public interface UserRepositorySupport {
List<User> findUsersByNameAndAge(String name, int age);
}
//UserRepositorySupportImpl
@Repository
@RequiredArgsConstructor
public class UserRepositorySupportImpl implements UserRepositorySupport {
private final JPAQueryFactory queryFactory;
@Override
public List<User> findUsersByNameAndAge(String name, int age) {
QUser user = QUser.user;
return queryFactory
.selectFrom(user)
.where(user.name.eq(name)
.and(user.age.eq(age)))
.fetch();
}
}
List<User> users = userRepositorySupport.findUsersByNameAndAge("홍길동", 20);
이렇게 복잡하게 들어가게 된다.
그래서 간단한 CRUD 같은 경우에는 JPA Repository를 사용하고 복잡한 쿼리문을 사용해야 하면 QueryDSL로 작성하면 된다.
☑️ JPA Repository / JPQL
JPQL은 Java Persistence Query Language의 약자로 JPA에서 사용하는 객체지향 쿼리 언어다.
@Query 어노테이션으로 사용하며 SQL은 테이블 기준이지만 JPQL은 엔티티(객체) 기준이다.
특정 DB 문법에 종속되지 않기 때문에 H2, MySQL, Oracle에 모두 사용 가능하다.
JPA Repository는 Spring Data JPA가 제공하는 인터페이스로 기본 CRUD 기능을 자동 제공하는 레포지토리 계층의 도구다.
즉 JPA Repository는 JPA를 쉽게 쓰도록 만들어진 인터페이스고 JPQL은 그 안에서 쿼리를 직접 작성할 때 사용하는 쿼리 언어다.
//JpaRepository
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByName(String name);
}
//JPQL
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.name = :name") // JPQL, @Query
List<User> findByName(@Param("name") String name);
}
☑️ JPQL / QueryDSL
JPQL은 JPA에서 사용하는 객체지향 쿼리 언어(문자열 기반)지만
QueryDSL은 Java 코드로 쿼리 + 타입 안전으로 작성하는 라이브러리다.
타입 안정성이 달라서 JPQL은 오타나 실수는 런타임 에러로 들어가지만 QueryDSL은 컴파일 시에 오류가 감지된다.
또 QueryDSL은 Q클래스 기반으로 IDE 자동완성을 지원한다.
JPQL은 Repository 인터페이스에서 @Query 어노테이션을 사용해 쿼리를 작성하고
ServiceImpl에서 해당 Repository를 직접 호출하여 사용한다.
QueryDSL은 RepositorySupport 인터페이스와 이를 구현한 RepositorySupportImpl 클래스를 따로 정의하고,
ServiceImpl에서 RepositorySupportImpl의 메서드를 호출하는 구조를 사용한다.
프로젝트에서도 QueryDSL을 사용했는데,
JPQL의 경우에는 문자열 기반이라 조건이 늘어나거나 필드명이 바뀔 경우 리팩토링이 어렵고 오타도 컴파일 타입에 잡히지 않기 때문에 유지보수에 불리하다고 판단했다.
반면에 QueryDSL은 타입 안정성과 IDE 자동완성 기능 덕분에 복잡한 조건에서도 안정성과 확장성, 유지보수가 용이하다고 판단했다.
출석자 리스트 조회 (classroomId + 날짜)를 한다는 예시로
JPQL
// JPQL
public List<AttendDto> findAttendByClassroomIdAndDateJPQL(Integer classroomId, LocalDate date) {
return entityManager.createQuery(
"SELECT new com.example.dto.AttendDto(s.userId, s.userName, s.userBirthday, a.attendTime) " +
"FROM ClassroomStudent s JOIN ClassroomAttend a ON s.userId = a.userId " +
"WHERE s.classroomId = :classroomId AND a.attendTime BETWEEN :start AND :end", AttendDto.class)
.setParameter("classroomId", classroomId)
.setParameter("start", date.atStartOfDay())
.setParameter("end", date.plusDays(1).atStartOfDay())
.getResultList();
}
QueryDSL
// QueryDSL
public List<AttendDto> findAttendByClassroomIdAndDateQueryDSL(Integer classroomId, LocalDate date) {
QClassroomStudent student = QClassroomStudent.classroomStudent;
QClassroomAttend attend = QClassroomAttend.classroomAttend;
return queryFactory
.select(Projections.fields(AttendDto.class,
student.userId,
student.userName,
student.userBirthday,
attend.attendTime
))
.from(student)
.join(attend).on(student.userId.eq(attend.userId))
.where(student.classroomId.eq(classroomId)
.and(attend.attendTime.goe(date.atStartOfDay()))
.and(attend.attendTime.lt(date.plusDays(1).atStartOfDay()))
)
.fetch();
}
이렇게 구현했는데 여기에 추가적으로 사용자 이름으로 정렬, 출석 시간으로 정렬, 페이지네이션 등등 기능을 추가해야 했다.
이런 기능들을 추가할 때 전체 쿼리문을 다시 작성할 필요 없이 기존 QueryDSL에 orderBy, offset, limit 메서드 추가로 해결했다.
// 기능 확장된 QueryDSL
public List<AttendDto> findAttendByClassroomIdAndDateQueryDSL(Integer classroomId, LocalDate date, Pageable pageable) {
QClassroomStudent student = QClassroomStudent.classroomStudent;
QClassroomAttend attend = QClassroomAttend.classroomAttend;
return queryFactory
.select(Projections.fields(AttendDto.class,
student.userId,
student.userName,
student.userBirthday,
attend.attendTime
))
.from(student)
.join(attend).on(student.userId.eq(attend.userId))
.where(student.classroomId.eq(classroomId)
.and(attend.attendTime.goe(date.atStartOfDay()))
.and(attend.attendTime.lt(date.plusDays(1).atStartOfDay()))
)
.orderBy(student.userName.asc(), attend.attendTime.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
}
만약 이걸 JPQL로 작성했더라면 정렬 기준이 바뀔 때마다 전체 쿼리를 문자열로 다시 작성해야 했을 것이고
페이징도 수동으로 구현했어야 해서 유지보수나 확장성 측면에서 부담이 컸을 것이다.
// JPQL 예시
public List<AttendDto> findAttendByClassroomIdAndDateJPQL(Integer classroomId, LocalDate date, int page, int size) {
String jpql = "SELECT new com.example.dto.AttendDto(s.userId, s.userName, s.userBirthday, a.attendTime) " +
"FROM ClassroomStudent s JOIN ClassroomAttend a ON s.userId = a.userId " +
"WHERE s.classroomId = :classroomId " +
"AND a.attendTime BETWEEN :start AND :end " +
"ORDER BY s.userName ASC, a.attendTime ASC"; // 정렬 조건 문자열에 직접 추가
return entityManager.createQuery(jpql, AttendDto.class)
.setParameter("classroomId", classroomId)
.setParameter("start", date.atStartOfDay())
.setParameter("end", date.plusDays(1).atStartOfDay())
.setFirstResult(page * size) // 수동 페이징: OFFSET
.setMaxResults(size) // 수동 페이징: LIMIT
.getResultList();
}
실제로 JPQL로 정렬과 페이징을 구현하려면 "ORDER BY 필드명"을 문자열에 직접 작성해야 하며,
필드명이 바뀌면 일일이 문자열을 수정해야 해 리팩토링에 매우 불리하다.
또한 페이징 역시 setFirstResult()와 setMaxResults()를 직접 사용해야 해 로직이 지저분해지기 쉽다.
이렇게 직접 QueryDSL을 사용해보면서 기능이 점차 확장될 가능성이 있는 쿼리는 처음부터 QueryDSL로 구현하는 것이
유지보수성과 확장성 면에서 훨씬 유리하다는 것을 확실히 체감할 수 있었다!
'Develop > JPA' 카테고리의 다른 글
| @OneToOne, @OneToMany (1) | 2025.07.21 |
|---|---|
| 암호화/복호화 알고리즘 KISA_SEED_CBC (0) | 2025.05.22 |
| 마이페이지 회원정보 수정 로직 (14) (2) | 2024.11.26 |
| 마이페이지 회원정보 수정로직 (13) (2) | 2024.11.25 |
| 마이페이지에 백엔드 작업하기 (11) (2) | 2024.11.21 |