엔티티 여러개를 만들고 서로 테이블끼리 조인을 할수록 복잡하고 길어지는 함수명을 만들 수 밖에 없다.
ex) List<> findListByTitleOrUserName(String title, String userName);
이런 경우, JPA의 자동매핑기능으로는 복잡한 쿼리를 해결하지 못하는 상황이 생길 수 있으며 네이티브 Query를 작성하거나 JPQL이라는 SQL을 추상화한 객체 쿼리 언어를 활용해야한다.
JPQL은 데이터베이스 테이블이 아닌 엔티티를 대상으로 쿼리를 작성하기에 객체 지향적인 쿼리 작성이 가능하며, JPA의 특성에 기반해 데이터베이스 독립성을 가질 수 있는 등, 다양한 장점이 있다.
그러나 문자열로 쿼리를 작성하기에 컴파일 시점에 쿼리의 오류를 검출할 수 없어 타입 안정성이 부족하고, 코드 가독성이 떨어진다. 가장 중요한 단점은 동적 쿼리 작성에 어려움이 있다는 것이다. 쿼리 조건이 여러 가지로 나뉘고, 동적으로 쿼리를 구성해야하는 상황에 JPQL은 적합하지 않다.
QueryDSL 개념
QueryDSL은 다양한 데이터베이스 플랫폼(RDBMS, No-SQL, …)에 접근하여 SQL과 유사한 문법으로 쿼리를 작성하여 데이터 처리를 수행하는데 도움을 주는 프레임워크이다.
타입-세이프(type-safe)하게 쿼리를 작성하도록 지원하며 SQL 형태가 아닌 ‘자바 코드’로 작성하여 데이터베이스 쿼리 작성을 쉽고 안전하게 만들어준다.
QueryDSL 특징
1.
타입 세이프 쿼리 생성
•
쿼리 작성 시 ‘컴파일 시점’에 쿼리 오류를 검출할 수 있다. 이는 런타임 시점에 발생할 오류를 사전에 방지하며 개발자의 생산성을 향상한다.
•
컴파일러를 통해 쿼리 문법 오류, 필드 이름 오타, 메서드 이름 오류등을 컴파일 단계에서 발견하여 런타임에서 발생할 오류를 방지하고 코드 품질을 높이는데 도움이 된다.
2.
동적 쿼리 지원
•
동적으로 쿼리를 작성하는 것을 지원한다. 특정 조건에 따라 쿼리의 WHERE 절에 동적으로 조건을 추가하거나 빼는 작업이 코드로 자연스럽게 가능하다.
•
예시
◦
QueryDSL의 값은 조건절(where)에서 null 값이 들어오는 경우 수행이 되지 않는다.
◦
그렇기에 파라미터 값이 존재하지 않는 경우 where절은 수행되지 않고, 존재하는 경우 수행된다.
/**
* 게시판 리스트를 전체 조회합니다.
*
* @param boardDto
* @return
*/
@Override
public List<BoardEntity> selectBoardList(BoardDto boardDto) {
BooleanBuilder builder = new BooleanBuilder();
JPAQuery<BoardEntity> jpaQuery = queryFactory
.select(board)
.from(board)
.where(
eqTitle(boardDto.getTitle()),
eqContent(boardDto.getContent())
)
.orderBy(board.boardSq.desc());
}
/**
* title 값이 존재하는 경우 조건절로 추가 됩니다.
*
* @param title {String}
* @return BooleanExpression
*/
private BooleanExpression eqTitle(String title) {
if (StringUtils.isNullOrEmpty(title)) {
return null;
}
return board.title.eq(title);
}
/**
* content 값이 존재하는 경우 조건절로 추가됩니다.
*
* @param content {String}
* @return BooleanExpression
*/
private BooleanExpression eqContent(String content) {
if (StringUtils.isNullOrEmpty(content)) {
return null;
}
return board.content.eq(content);
}
Java
복사
3.
자동 완성 기능 지원
•
쿼리를 작성하는 동안에 IDE의 자동완성 기능을 통해서 쿼리를 쉽게 작성하고 유지 관리할 수 있다.
•
또한 다양한 데이터베이스 백엔드 환경에서 ‘표준화된 쿼리’를 작성하여 간결하고 가독성을 높이며 오류를 줄여준다.
4.
다양한 데이터베이스 백엔드 지원
•
이러한 데이터베이스 백엔드로 JPA, JDO, SQL, Hibernate, MongoDB, Lucene, Collection과 같은 백엔드에 대한 쿼리를 작성할 수 있도록 지원한다.
•
이로 인해 특정 데이터베이스에 종속되지 않고 다양한 데이터베이스 환경에서 QueryDSL을 사용할 수 있다.
// 구성한 JPAQueryFactory를 로드
private final JPAQueryFactory queryFactory;
QBoardEntity board = QBoardEntity.boardEntity;
queryFactory
.select(board)
.from(board)
.where(
eqTitle(boardDto.getTitle()),
eqContent(boardDto.getContent())
)
.orderBy(board.boardSq.desc())
.fetch();
Java
복사
MySQL을 이용한 데이터베이스 Query 구성
// 모델 클래스인 'Person'에 대한 메타 모델 'QPerson'의 인스턴스를 생성
QPerson person = new QPerson("person");
// MongoDBQueryFactory 객체를 생성. 이 객체를 사용하여 MongoDB 쿼리를 작성하고 실행
MongoDBQueryFactory queryFactory = new MongoDBQueryFactory(mongoClient, "myDatabase");
// 쿼리를 작성하고 실행
List<Person> adults = queryFactory.selectFrom(person)
.where(person.age.goe(18)) // age가 18 이상인 사람을 조회
.fetch(); // 쿼리 실행 및 결과 반환
Java
복사
QueryDSL 구성 요소
•
DSL(Domain-Specific Languages)
‘특정 도메인’에 초점을 맞춘 소프트웨어 언어. 이를 이용하여 특정 문제를 해결하기 위해 특정 문제 영역에 관련된 작업을 수행하는 데 사용된다.
•
메타 모델 Q-Class
쿼리 타입 혹은 Q-Class라는 이름으로 불리며 ‘도메인 모델의 구조’를 나타낸다. 주로 도메인 클래스에 대한 쿼리를 작성할 때 사용되며 도메인 클래스의 각 필드를 변수로 가지고 있다.
예를 들어, UserEntity라는 엔티티 클래스가 있다면 메타 모델로 QUserEntity 클래스로 생성이 된다.
•
JDO(Java Data Objects)
자바 애플리케이션에서 데이터베이스에 저장된 데이터에 접근하고 조작하는 API이다.
JDO는 객체 지향적인 접근을 제공하므로, 개발자는 SQL 쿼리를 직접 작성하지 않고도 데이터베이스의 데이터에 접근할 수 있다.
JDO 사용 예시
PersistenceManagerFactory pmf = JDOHelper.getPersistenceManagerFactory("transactions-optional");
PersistenceManager pm = pmf.getPersistenceManager();
// 1
QCustomer customer = QCustomer.customer;
// 2
JDOQuery<?> query = new JDOQuery<Void>(pm);
// 3
Customer bob = query.select(customer)
.from(customer)
.where(customer.firstName.eq("Bob"))
.fetchOne();
// 4
query.close();
Java
복사
QueryDSL을 활용한 JPA 내부 동작 구조
개발자(클라이언트)가 JPQL을 문자열로 직접 작성하면, 타입 안정성을 체크할 수 없고 직관적인 동적쿼리도 작성할 수 없다. QueryDSL은 JPQL을 대신 생성하며 이런 문제를 해결한다.
1.
개발자의 요청
개발자는 QueryDS이 제공하는 스펙 (메서드 체인, 타입 안전한 쿼리 작성 방식)을 사용하여 쿼리를 작성한다. 이 요청은 자바 코드로 표현되며, 개발자는 Q 클래스를 사용하여 조건을 정의하고 JPAQueryFactory와 같은 API를 통해 쿼리를 생성한다.
2.
QueryDSL 프로세스
개발자가 작성한 쿼리는 QueryDSL 프로세스에 의해 처리된다. QueryDSL 프로세스는 자바 코드 기반의 쿼리 요청을 분석하고, 이를 적절한 SQL이나 JPQL로 변환하는 역할을 한다. 이 과정에서 컴파일 타임에 쿼리의 오류를 검출하고, 동적 쿼리의 조건을 처리하게 된다.
3.
EntityManager 사용
생성된 JPQL 쿼리는 EntityManager로 전달된다. EntityManager는 JPA의 핵심 구성 요소로, JPQL을 실행하고 결과를 반환하는 역할을 한다. 즉, QueryDSL을 통해 작성된 쿼리는 EntityManager에 의해 데이터베이스와 상호작용하게 된다.
4.
DB 실행 및 결과 반환
EntityManager에서 JPQL을 실제 SQL로 변환하여 DB로 전달하고, DB는 쿼리를 실행하여 요청된 데이터를 반환한다. 이 결과는 일반적으로 Entity나 List<Entity>의 형태로 반환되며, 개발자는 이를 자바 컬렉션으로 받아서 사용할 수 있다.
QueryDSL 프로세스
1.
서버 컴파일
서버는 엔티티 클래스를 기반으로 APT (Annotation Processing Tool)를 사용하여 컴파일을 수행한다. 이는 컴파일 시점에 자동으로 발생.
엔티티 클래스에 변화가 발생하면, 빌드를 다시 수행하여 메타 모델 (Q-Class)과 동기화를 맞춘다.
2.
JPAQueryFactory 구현
JPAQueryFactory를 설정한다. 이는 QueryDSL을 사용하는 데 필요한 클래스로, JPAQuery의 인스턴스를 생성한다.
이를 통해 실제 데이터베이스에 대한 쿼리를 작성하고 실행할 수 있다. 즉, JPAQueryFactory는 데이터베이스 쿼리를 생성하고 실행하는 메커니즘을 제공하는 중요한 구성요소이다.
@Configuration
@EnableJpaAuditing
public class JPAConfig {
@PersistenceContext
private EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
Java
복사
3.
메타 모델 구성 : Q-Class
쿼리 타입 혹은 Q-Class라는 이름으로 불리며 ‘도메인 모델의 구조’를 나타낸다. 주로 도메인 클래스에 대한 쿼리를 작성할 때 사용되며 도메인 클래스의 각 필드를 변수로 가지고 있다.
•
예를 들어 UserEntity라는 엔티티 클래스가 있다면 메타 모델로 QUserEntity 클래스로 생성된다.
•
QueryDSL의 코드 생성 도구를 통해 자동으로 생성될 수 있다. 일반적으로 컴파일을 수행하는 단계에 변화된 엔티티를 분석하여 해당하는 Q-Class를 생성해준다.
•
이렇게 생성된 Q-Class를 사용하면 쿼리 작성 시 컴파일 타임에 오류를 잡을 수 있으며 IDE의 코드 자동완성 기능을 활용할 수 있어 편리하다.
•
하지만 Q-Class는 도메인 클래스의 구조가 변경될 때마다 재생성해야 한다. 그렇지 않으면, 도메인 클래스와 Q-Class 사이에 불일치가 생기게 되어 쿼리 오류를 일으킬 수 있다.
a.
개발자는 컴파일을 수행한다.
build.gradle 파일 내에 작성한 태스크가 수행된다.
b.
컴파일을 수행하는 과정에서 코드 생성 도구(APT)가 수행된다.
→ com.querydsl:querydsl-apt 라이브러리를 이용하여 소스코드를 생성한다.
c.
컴파일이 완료되면 QUserEntity등의 엔티티 클래스와 매핑되는 Q-Class가 생성된다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
// Getters, Setters, 생성자, toString..
}
Java
복사
// QUser.java (자동 생성된 코드)
public class QUser extends EntityPathBase<User> {
public static final QUser user = new QUser("user");
public final NumberPath<Long> id = createNumber("id", Long.class);
public final StringPath name = createString("name");
public final NumberPath<Integer> age = createNumber("age", Integer.class);
public QUser(String variable) {
super(User.class, forVariable(variable));
}
// 여러 생성자 및 메서드가 추가될 수 있다.
}
Java
복사
4.
쿼리문 구성 : JPAQuery
실제 데이터베이스에 대한 쿼리를 작성하고 실행하는 역할을 한다. (’select’, ‘where’, ‘groupBy’, …)
@Repository
public class BoardDaoImpl implements BoardDao {
// 구성한 JPAQueryFactory를 로드
private final JPAQueryFactory queryFactory;
public BoardDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
/**
* 게시판 리스트를 전체 조회합니다.
*
* @param boardDto
* @return
*/
@Override
public List<BoardEntity> selectBoardList(BoardDto boardDto) {
QBoardEntity board = QBoardEntity.boardEntity;
JPAQuery<BoardEntity> jpaQuery = queryFactory
.select(board)
.from(board)
.where(board.boardSq.eq(boardDto.getBoardSq()))
.orderBy(board.boardSq.desc());
}
}
Java
복사
5.
특정 필드 조회 : projection
엔티티 내에 특정 필드 값만 조회하는 방법으로 Projection을 사용한다. 이를 통해 필요한 정보만 효율적으로 가지고 올 수 있다.
Tuple, Projections.field, Projections.beans, Projections.constructor, @QueryProjection 등을 이용하여 각각 필드에 맞는 값을 조회할 수 있다.
QUserEntity user = QUserEntity.userEntity;
JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
List<UserDTO> userDTOs = queryFactory
.select(Projections.constructor(UserDTO.class, user.name, user.age))
.from(user)
.fetch();
Java
복사
메서드 정리
6.
결과값 구성 : fetchable
QueryDSL에서 제공하는 인터페이스로, 쿼리의 결과를 가져오는 메서드를 정의하고 있다.
이는 주로 QueryDSL 쿼리의 마지막 부분에서 사용되며, 쿼리를 실행하고 그 결과를 반환한다.
메서드 정리