Spring 에서 DB와 연동하여 사용해야 할 때 Repository 를 만들고, JPA 를 상속받도록 하여 JPA 가 기본적으로 제공하는 메서드를 사용할 수 있도록 한다. JPA의 메서드 명명 규칙에 따라 find, remove, delete 등의 메서드를 자유롭게 쓸 수 있는 것이다.
혹은 복잡한 쿼리문이 필요하다면 JPQL 을 통해 어노테이션이나 문자열로 쿼리문을 작성할 수도 있다.
하지만 JPQL엔 단점도 몇 가지 있다.
1. JPQL은 문자열이기 때문에 문법이 틀려도 바로 알아채기 어렵고, JPQL 을 파싱하는 단계에서 문법 오류를 발견할 수 있다.
2. 직관적인 동적 쿼리 작성이 어렵다. 조건이 많아질수록, 단계가 길어질수록 중간중간 끊어가며 값을 확인하거나 다시 변환해야 하는 등의 작업이 생기기 때문이다.
이런 문제를 해결해 주는 것이 바로 QueryDSL 이다.
사실 위의 JPQL 을 사용하는 과정은 개발자가 쿼리문을 작성하면 EntityManager 가 이를 확인하고 실행하여 DB에서 결과를 가져다가 Entity 를 전달해주는 방식으로 수행된다.
이때 QueryDSL 의 메서드를 사용하는 것으로 더 쉽게 쿼리문을 작성할 수 있고, 우리가 작성한 쿼리문을 QueryDSL 이 직접 JPQL 로 변환해서 EntityManager 에게 전달하기 때문에 도중에 발생한 문법적 오류나 타입 안정성도 체크받을 수 있다.
QueryDSL 을 사용하기 위해선 JPQL 을 생성할 수 있도록 필요한 데이터(Entity) 정보를 전달해야하는데, JPA 프레임 워크와는 분리되어 있기 때문에 @Entity 를 직접 사용하는 것이 아니라 QueryDSL 플러그인 설치 시 자동으로 생성되는 Q타입 클래스를 사용한다.
( build > generated > source > kapt > main > domain > model > Q~~~ )
이제 QueryDSL 을 설치하고 사용해보자.
kotlin("kapt") version "1.8.22"
val queryDslVersion = "5.0.0"
implementation("com.querydsl:querydsl-jpa:$queryDslVersion:jakarta")
kapt("com.querydsl:querydsl-apt:$queryDslVersion:jakarta")
abstract class QueryDslSupport {
@PersistenceContext
protected lateinit var entityManager: EntityManager
protected val queryFactory : JPAQueryFactory
get(){
return JPAQueryFactory(entityManager)
}
}
@PersistenceContext 어노테이션을 통해 EntityManager 를 주입받을 수 있도록 하고, queryFactory 를 통해 JPAQueryFactory 에 entityManager 를 담은 인스턴스를 반환하여 이 클래스를 상속받는 곳에서 queryFactory 를 사용할 수 있도록 한다.
QueryDslCardRepository 같은 이름으로 새로운 레포지토리 클래스를 생성하여 QueryDslSupport 를 상속 받아 사용할 수도 있지만 그러면 기존 CardRepository 에 추가로 의존성 을 주입받아야 하기 때문에 기존 Repository 를 수정하는 방법을 사용해 보자. 구조는 다음과 같다.
CardRepository 는 Jpa 를 상속받아 기본 명명규칙에 따라 메서드를 만들고 사용할 수 있다.
여기에 queryDsl 을 사용하는 사용자 정의 메서드를 추가하고 싶기 때문에 CustomCardRepository 를 상속 받도록 하고, 정의는 CardRepositoryImpl 에서 하기로 한다.
CardRepositoryImpl 은 Jpa 가 자동으로 생성하는 레포지토리의 구현체로, 원한다면 개발자가 직접 정의하여 사용하는 것도 가능하다. ( CardRepository 상속받을 필요 없음 ! )
CardRepositoryImpl 에서는 사용자 정의 메서드를 구현하기 위해 다시 CustomCardRepository 를 상속받아 오버라이드하여 구현 부분을 작성한다.
사용자 정의 메서드를 구현하기 위해선 QueryDslSupport() 의 queryFactory를 불러와 dsl쿼리 명령어로 작성할 수 있다. 이때, Q타입 클래스를 테이블 처럼 사용하게 된다.
이 과정을 통해 CardRepository 내부에는 Jpa 메서드와 사용자 정의 메서드를 함께 포함하고 있기 때문에 Service 에선 CardRepository 만 주입받아 사용할 수 있는 것이다!
이후에 새로 추가하고 싶은 사용자 정의 기능이 생긴다면 CustomCardRepository 에 추가하고, Impl 에 구현하며 진행하면 외부에는 변화 없이 계속해서 기능을 추가할 수 있게 된다.
class CardRepositoryImpl : CustomCardRepository, QueryDslSupport() { // 괄호 안에 아무 값도 전달하지 않고 기본 생성자 호출
private val card = QCard.card
override fun searchCardByTitle(title: String): List<Card> {
return queryFactory.selectFrom(card)
.where(card.title.containsIgnoreCase(title)) // 대소문자 무시
.fetch()
}
}
처음 Repository 의 구현체와 상속 과정이 너무 헷갈리고 이유를 알 수 없어 한참을 메달리고 나서야 이해할 수 있었다. 특히 Impl 을 자동으로 생성해서 사용한다는 점이 Service - ServiceImpl 의 관계와 비교해서 이해하려 하다보니 더 헷갈린 것 같다. ( 참고로 ServiceImpl 은 편의상 명명된 것으로 이름은 달라도 되고, 상속과 @Service 어노테이션만 잘 붙이면 된다. )
새 구조를 익힐 때 마다 그냥 넘기지 말고 꼭 과정과 이유를 파악하자.