JPA 에서 조건 조회 할 수 있는 다섯번 째, QueryDSL 입니다.
1. Query Method
2. @Query
3. Criteria API
4. Specification
QueryDsl 은 Java에서 다양한 데이터베이스에 대해 타입 안전하고, 직관적인 쿼리를 작성할 수 있게 해주는 프레임워크입니다. JPQL이나 SQL을 자바 코드로 표현할 수 있는 도메인 특화 언어(DSL)를 제공하며, 이는 코드 자동 완성과 컴파일 타임에 타입 검사를 지원하므로 쿼리 작성 시 발생할 수 있는 오류를 줄여줍니다.
Java에서는 객체 지향적으로 데이터베이스 쿼리를 작성하기 위해 JPA(Java Persistence API)를 자주 사용합니다. 하지만 JPA의 표준 쿼리 언어인 JPQL(Java Persistence Query Language)은 문자열 기반으로 작성되며, 복잡한 쿼리를 작성하거나 유지보수하는 데 어려움이 있습니다. 이를 해결하기 위해 등장한 것이 Criteria API와 QueryDSL입니다.
Criteria API는 JPA 2.0에서 도입된 동적 쿼리 작성 도구로, 타입 안전하게 쿼리를 작성할 수 있지만 코드가 복잡하고 가독성이 떨어지는 단점이 있습니다. 이로 인해 좀 더 직관적이고 읽기 쉬운 쿼리 작성 방법에 대한 요구가 생겼고, 이러한 요구를 충족시키기 위해 QueryDSL이 등장하게 되었습니다.
Springboot 3.x, gradle 적용 방법입니다.
build.gradle
파일에 dependency 를 추가합니다.
dependencies {
//생략..
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"
//생략..
Reload Gradle Project 를 진행해서 라이브러리를 다운받은 후에 ReBuild 를 진행해 주세요. (단축키 Alt + B, R)
Build 가 끝나면 build.generated.sources.annotationProcessor.java.main.{Base Package Entity Path}
에 QEntity
들이 생성이 됩니다.
QueryDSL을 사용할 때 가장 중요한 개념 중 하나가 바로 Q클래스(QEntity)입니다. QEntity 클래스는 QueryDSL에서 자동으로 생성되는 클래스들로, 데이터베이스 엔티티를 자바 코드에서 타입 안전하게 다룰 수 있도록 도와줍니다. 예를 들어, Member이라는 JPA 엔티티가 있다고 가정하면, QueryDSL은 이 엔티티를 기반으로 QMember이라는 클래스를 생성합니다. 이 클래스는 Member 엔티티의 필드들을 기반으로 각 필드에 접근할 수 있는 타입 안전한 속성을 제공합니다. 빌드도구 설정을 통해 QEntity Class가 자동으로 생성되며, 이를 활용해 직관적이고 안전한 쿼리를 작성할 수 있습니다.
기존에 작성했던 Criteria API 를 사용했던 코드를 QueryDsl 로 바꿔보도록 하겠습니다. 이전 코드는 Link 를 확인하세요.
domain.member.service.MemberQueryDslServiceImpl
@Service
public class MemberQueryDslServiceImpl implements MemberService {
@Override
public MemberSearchResponse getMemberById(String memberId) {
return null;
}
@Override
public List<MemberSearchResponse> getMembersByMemberName(String memberName) {
return null;
}
@Override
public List<MemberSearchResponse> getMembers() {
return null;
}
@Override
public MemberCreateResponse createMember(MemberCreateRequest parameter) {
return null;
}
@Override
public MemberModifyResponse modifyMember(MemberModifyRequest parameter) {
return null;
}
@Override
public Long deleteMember(String memberId) {
return null;
}
}
여기서 MemberId 로 조회하는 getMemberById Method 를 QueryDsl로 구현해보겠습니다.
private final MemberMapper memberMapper = Mappers.getMapper(MemberMapper.class);
@PersistenceContext
private EntityManager entityManager;
@Override
public MemberSearchResponse getMemberById(String memberId) {
QMember qMember = QMember.member;
Optional<Member> optionalMember
= Optional.ofNullable(new JPAQueryFactory(entityManager)
.selectFrom(qMember)
.where(qMember.memberId.eq(memberId))
.fetchOne());
if (optionalMember.isEmpty()) {
throw new ServiceException(ErrorCode.NO_DATA, "회원 정보가 존재하지 않습니다.");
}
Member member = optionalMember.get();
return memberMapper.toRecord(member);
}
위 코드에서 qMember.memberId.eq(memberId) 와 같은 표현이 가능한 이유는 memberId 필드를 StringPath type 으로 접근할 수 있도록 하는 QClass 때문입니다. QClass 를 사용하면 컴파일 타임에 타입오류를 발생시키기 때문에 타입오류를 방지할 수 있고, IDE 자동완성으로 오타를 줄일 수 있으며, 메서드 체이닝 방식을 사용해 쿼리의 가독성을 높힐 수 있습니다. Criteria API 에 비해 더 직관적이고, 가독성이 높은 것을 확인하실 수 있습니다.
memberId 하나의 필드만 조건 검색하는 코드라 정확히 차이가 안느껴지실 수 있겠지만, 쿼리가 복잡해지고 많은 컬럼들에 대한 조건을 추가할 수록 QueryDsl의 장점은 부각될 것 입니다.
Optional<Member> member = memberRepository.findById(memberId);
@Query("select m from member m where m.memberId = :memberId")
Member findMemberById(@Param("memberId") String memberId);
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Member> criteriaQuery = criteriaBuilder.createQuery(Member.class);
Root<Member> rootMember = criteriaQuery.from(Member.class);
Predicate memberIdEqual =
criteriaBuilder.equal(rootMember.get("memberId"), memberId);
criteriaQuery.select(rootMember)
.where(memberIdEqual);
Member result = entityManager.createQuery(criteriaQuery).getSingleResult();
public List<MemberSearchResponse> getMemberById(String memberId) {
List<Member> members = memberRepository.findAll(
MemberSpecification.memberId(memberId)
);
return memberMapper.toRecordList(members);
}
public class MemberSpecification {
public static Specification<Member> memberId(String memberId) {
return (root, query, criteriaBuilder) ->
(StringUtils.isEmpty(memberId)) ? criteriaBuilder.conjunction()
: criteriaBuilder.equal(
root.get("memberId"),
memberId);
}
}
QMember qMember = QMember.member;
Optional<Member> optionalMember =
Optional.ofNullable(new JPAQueryFactory(entityManager)
.selectFrom(qMember)
.where(qMember.memberId.eq(memberId))
.fetchOne());
queryDsl 에서도 N+1 문제는 발생합니다. 그 해결방법을 알아봅시다.
연관관계에 있는 Entity 간 fetchJoin()
을 사용하여 N+1 문제를 해결합니다.
@Override
public List<MemberSearchResponse> getMembers() {
QMember qMember = QMember.member;
QAuthority qAuthority = QAuthority.authority;
List<Member> members = new JPAQueryFactory(entityManager)
.selectFrom(qMember)
.leftJoin(qMember.authority, qAuthority).fetchJoin()
.fetch();
return memberMapper.toRecordList(members);
}
Entity Graph가 이미 작성되어 있다면 setHint
로 전달하여 N+1 문제를 해결합니다.
@Override
public List<MemberSearchResponse> getMembers() {
QMember qMember = QMember.member;
List<Member> members = new JPAQueryFactory(entityManager)
.selectFrom(qMember)
.setHint("jakarta.persistence.loadgraph"
, entityManager.getEntityGraph("memberGraph"))
.fetch();
return memberMapper.toRecordList(members);
}
entity.Member
@NamedEntityGraph(
name = "memberGraph",
attributeNodes = {
@NamedAttributeNode("authority")
}
)
public class Member extends BaseEntity implements Persistable<String> {
@Id
@Column(name = "member_id")
private String memberId;
//중간 생략..
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "authority_cd")
private Authority authority;
다른 방버들과 같이 @BatchSize 를 설정하면 N+1 문제를 해결할 수 있습니다. 자세한 내용은 Link를 확인하세요!
QueryDSL은 타입 안전하고 직관적인 쿼리 작성 방식을 제공함으로써, 복잡한 SQL 쿼리 작성의 부담을 줄여줍니다. JPA Criteria API에 비해 더 가독성 높은 코드를 작성할 수 있고, 특히 대규모 프로젝트에서 쿼리 작성의 일관성을 유지하는 데 큰 도움이 됩니다. 하지만 QueryDSL을 사용할 때도 성능 최적화를 고려해야 하며, N+1 문제와 같은 성능 이슈를 피하기 위해 fetch join이나 setHint와 같은 기법을 적절히 활용하는 것이 중요합니다. QueryDSL을 잘 활용하면, 더욱 효율적이고 유지보수 가능한 애플리케이션을 개발할 수 있습니다.