JPA Repository에서 기본적으로 제공해주는 메소드들과 JPQL을 사용을 하다가 단점들이 너무 많이 보여서 대체제를 찾기 위해 동적쿼리에 대해 알아보고 공부를 하였습니다.
▪ 정적쿼리 : 조건에 상관없이 변하지 않는 쿼리
▪ 동적쿼리 : 특정 조건에 따라 변경되는 쿼리
정적쿼리 만으로는 비즈니스 로직을 수행하기가 어렵습니다.
따라서 동적쿼리를 사용하는 경우가 많기 때문에 스프링에서는 어떠한 동적쿼리가 있는지 알아봅시다.
// entityManager를 통한 JPQL 실행
public List<Member> jpqlTest(String role) {
String jpql = "select m from Member m";
String whereSql = " where ";
if (StringUtils.equls(role, "USER")) {
jpql += whereSql + "m.role = :role";
}
TypedQuery<Member> query = entityManager.createQuery(jpql, Member.class);
query.setParameter("role", role);
...
List<Member> members = query.getResultList();
return members
}
// JPA Repository
public interface PostLikeRepository extends JpaRepository<PostLikeEntity, Long>{
@Query("select postLike from PostLikeEntity postLike " +
"where postLike.user.userId = :userId and postLike.post.postId = :postId")
PostLikeEntity findByUserIdAndPostId(Long userId, Long postId);
}
@Query
어노테이션을 통한 JPQL 작성 및 실행가장 큰 단점은 문자열을 통해서 동적쿼리를 작성하고 있다는 것입니다.
이 뜻은 문자열로 작성한 쿼리문에 대한 문법 오류를 잡아주지 못하는 단점이 있습니다.
쿼리를 실행하기 전까지는 개발자의 실수들이 얼마든지 침범할 수 있는 여지를 남기고 있습니다.
Criteria는 JPQL을 문자열이 아닌 자바 코드로 작성하도록 도와주는 빌더 클래스 API입니다.
// Criteria만 사용 시
public List<Member> testCriteria(String role) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Member> cq = builder.createQuery(Member.class);
Root<Member> root = cq.from(Member.class);
List<Predicate> predicates = new ArrayList<>();
if (StringUtils.equls(role, "USER")) {
predicates.add(builder.equal(root.get("role"), role));
}
cq.where(predicates.toArray(new Predicate[0]));
...
TypedQuery<Member> query = entityManager.createQuery(cq);
List<Member> members = query.getResultList();
return members;
}
// Specification & Criteria 사용 시
public interface MemberRepository extends JpaRepository<Member, Long>, JpaSpecificationExecutor<Member> {
List<Member> findAll(Specification<Member> spec);
}
public void testQuery() {
List<Member> members = memberRepository.findAll(
memberSpec.makeSpecification(role)
);
}
public Specification<Member> makeSpecification(String role) {
return ((root, query, builder) -> {
List<Predicate> predicates = new ArrayList<>();
if (StringUtils.equls(role, "USER")) {
predicates.add(builder.equal(root.get("role"), role));
}
...
return builder.and(predicates.toArray(new Predicate[0]));
});
}
Criteria만 사용했을 때는 JPQL과 비교를 하면 JPQL의 단점을 해결하였습니다.
하지만 쿼리를 생성하기 위한 코드가 복잡하기 때문에 가독성이 많이 떨어집니다.
만약 저러한 조건들이 많다면 코드는 그만큼 더 복잡해지겠죠
Spring Data JPA에서는 Criteria 사용을 높이기 위해 Specification
를 제공하고 있습니다. 이를 사용하기 위해서는 JPA Repository에서 JpaSpecificationExecutor
상속 받아야합니다.
Specification 과 Criteria 를 함께 쓰게된다면 Criteria만 사용했을 때보다 훨씬 가독성이 좋은 모습을 볼 수 있습니다.
하.지.만. 그래도 여전히 코드의 형태가 SQL 적이지 않고 직관적이지 않습니다.
저 코드를 분석하면 어떠한 쿼리문이 실행되겠다라고 이해를 하겠지만 분석 하는데 시간을 소요하게 됩니다.
public List<Member> findMember(String role) {
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.equls(role, "USER")) {
builder.and(member.gitHubId.contains(gitHubId));
}
JPAQuery<Member> jpaQuery = jpaQueryFactory
.selectFrom(member)
.where(builder);
return jpaQuery.fetch();
}
보시면 알겠지만 진짜 SQL 작성하는 것처럼 코드를 작성하게 되어서 직관적입니다.
QueryDSL은 Q.Class에 의존하여 쿼리를 생성하는 것도 단점이라고 볼 수 있습니다. Q.Class를 생성하는 방법이 버전과 빌드 방식에 따라 달라진다는 것입니다. Gradle 빌드해서 생성하는 방식도 있고 Annotation Processor 사용하는 방식도 있습니다.
하지만 설정만 잘해준다면 QueryDSL은 괜찮은 동적쿼리 방식인 것 같습니다.
JPA Repository의 단점을 매우 느끼고 있는 중에 동적쿼리를 공부하게 되었습니다.
이렇게 동적쿼리를 사용한다면 다양한 서비스들을 만들 수 있을 것 같습니다.
제가 나중에 프로젝트를 하게 된다면 QueryDSL 또는 Mybatis 사용을 선호할 것 같습니다.
그리고 사용할 떄는 추가적으로 깊게 다시 공부할 것 같습니다.
나중에 Mybatis도 한번 공부해서 블로그 글을 올릴 예정입니다.
(솔직히 실제 SQL이라 가장 좋을지도 읍읍)