카테고리 없음

2024-06-07 [JPQL 보다 QueryDSL?]

Glen_check 2024. 6. 7. 21:49

전주 팀 프로젝트 진행했을 당시 Post 를 전체 조회 시 Post  별 Like 의 수도 함께 조회할 수 있도록 설정하는 부분에서 JPQL 을 시도해 봤었는데, 결국에는 실패하고, 코드 상으로 해당 기능을 구현했던 것이 생각난다.

해당 기능을 구현하지 못한 데에는 여러 가지 이유가 있었는데 우선 그 때 작성한 Query 를 아래에 공유드립니다.

@Query(
    nativeQuery = true,
    value = "SELECT p.id, p.title, p.content, p.user, p.createdAt, COUNT(l.id) AS likeCount FROM Post p LEFT JOIN PostLike l ON p.id = l.post.id GROUP BY p.id, p.title, p.content, p.user, p.createdAt ORDER BY p.createdAt DESC"
)

SupaBase 로 직접 Table 을 만들어 Test 를 해봤을 때도 문제없이 의도한대로 잘 조회가 되었는데, 그래서 사실 아직까지 정확한 원인을 모르겠다.

그래서 예측하건대, 해당 코드에는 없지만 응답 타입을 결정할 때 Entity 가 아닌, ResponseDto 를 넣어 추가적으로 매핑하는 과정이 필요했고, 그러려면 추가적인 코드 작성이 필요했던 것으로 보인다.

그러나 그 당시에는 오류를 알 수 없던 것이 우선 String 으로 작성하기 때문에 컴파일 시 오류 발생없이 잘 시행된다는 점, 그리고 Test 시 명확하게 오류 발생 원인을 알 수 없다는 문제가 있었습니다.

 

그리고 금일 QueryDSL 관련 공부를 진행하면서 왜 실무에서 JPQL 보다 QueryDSL 을 더욱 선호하는 몸소 느낄 수 있었습니다.

여러 글들을 찾아봤을 때 많은 사람들이 강조하는 부분이

'문자열 형태로 작성해야 하기 때문에, 동적인 쿼리 작성이 힘들다' 인데, 해당 부분도 함께 다뤄보려 합니다.

 

QueryDSL 이 뭔데?

SQL, JPQL 등을 코드로 작성할 수 있도록 해주는 Framework 로, Query 를 type-safe(컴파일시 에러 체크 가능)하게 Java 코드로 작성할 수 있습니다. QueryDSL은 동적 쿼리를 아주 편리하게 작성할 수 있어, 복잡한 동적 쿼리를 사용해야 할 때 QueryDSL을 사용하고 단순한 경우에는 Spring Data JPA를 사용합니다.

QueryDSL 에서는 JPA에서 관리되는 Entity 를 활용하기 보다 static한 QClass를 사용합니다. QClass 는 Entity를 기반으로 자동으로 생성되는 Class입니다.

(QueryDSL 은 JPA가 제공하는 JPQL 을 코드로 작성할 수 있도록 도와주는 빌더 역할을 하기 때문에 JPQL의 문법에 대한 이해가 동반되어야 합니다.)

 

SQL, JPQL 의 문제점

QueryDSL 을 이해하기 전 SQL 과 JPQL 의 문제점을 확인해보려 합니다.

# SQL
String sql = "select id, title, content, writer from post";
 
# JPQL
String jpql = "select p from Post p";

SQL, JPQL 은 문자열로 Type-check 가 불가능합니다. 그래서 만약  Query 에 오류가 있을 시 컴파일 시점에 오류를 잡을 수 없어 애플리케이션을 실행한 후에야 오류를 발견할 수 있습니다.

 

Qclass

QueryDSL은 컴파일 단계에서 Entitiy를 기반으로 QClass를 생성하는데,

JPAAnnotationProcessor 가 컴파일 시점에 작동해서 @Entity 등등의 어노테이션을 찾아 해당 파일들을 분석해서 QClass를 만들어 줍니다.

그리고 이 Entity Class 와 매핑되는QClass 를 기반으로 쿼리를 실행합니다.

아래와 같이 사용할 수 있습니다.

val post = QPost.post // 기본 instance
val post = QPost("p") // 별칭 지정

queryFactory.select(post).from(post).fetch()

queryFactory.selectFrom(post).fetch()

// Title 만 조회 - List<String> 형태로 조회됨
queryFactory.select(post.title).from(post).fetch()

// Title, numLike 만 조회 - List<Tuple> 형태로 조회됨
queryFactory.select(post.title, post.numLike).from(post).fetch()

 

QueryDSL 장점

문자가 아닌 코드로 작성하여 컴파일 시점에 오류를 잡을 수 있습니다.

또한 select, from, where 등 쿼리 작성에 필요한 키워드들을 메서드 형식으로 제공하여 단순하고 쉽게 작성할 수 있습니다.

QueryDSL은 주로 JPA와 함께 많이 사용하며 가장 장점이라 할 수 있는 부분은 동적 쿼리를 아주 편리하게 작성할 수 있습니다.

 

아래는 동적 쿼리를 작성한 예시입니다.

@Service
class UserService {

    @PersistenceContext
    private lateinit var em: EntityManager

    private val queryFactory by lazy { JPAQueryFactory(em) }

    fun findUsers(username: String?, email: String?): List<User> {
        val user = QUser.user

        val predicate = BooleanBuilder()
        if (username != null) {
            predicate.and(user.username.eq(username))
        }
        if (email != null) {
            predicate.and(user.email.eq(email))
        }

        return queryFactory.selectFrom(user)
            .where(predicate)
            .fetch()
    }
}

UserService Class 에서 JPAQueryFactory 를 사용하여 동적 쿼리를 생성합니다.

BooleanBuilder 를 사용하여 조건을 동적으로 추가하고, username 과 email 이 null 이 아닌 경우에만 조건을 추가하여 동적 쿼리를 다음과 같이 쉽게 작성할 수 있습니다.

 

코드 풀이를 통해 더 자세히 이해해보자면,

 

findUsers Method 는 사용자 이름과 이메일을 기준으로 User 엔티티를 검색하는 기능을 제공합니다.

QUser.user 는 QueryDSL 이 생성한 Qclass 인스턴스입니다. 이는 이전 설명한 것과 같이 User 엔티티의 속성에 접근할 수 있도록 합니다.

 

*BooleanBuilder 는 여러 조건을 동적으로 결합하는데 사용되며, username 과 email 이 null 이 아닌 경우에만 해당 조건을 추가합니다.

user.username.eq(username) 는 username 필드가 주어진 값과 일치하는지 확인하는 조건을 생성합니다.

predicate.and 메서드를 사용하여 각 조건을 BooleanBuilder에 추가합니다.

 

BooleanBuilder : QueryDSL 에서 여러 조건을 결합하여 하나의 BooleanExpression(불리언 표현식)을 만드는 데 사용되는 클래스입니다.

동적 쿼리를 작성할 때 매우 유용하며, BooleanBuilder 를 사용하면 조건을 단계적으로 추가하거나 제거할 수 있어, 복잡한 조건문을 유연하게 처리할 수 있다는 장점이 있습니다!

 

벌크 수정, 벌크 삭제

JPA 에서는 Dirty Checking 을 통해 Entity의 변경을 감지하고, update , delete 쿼리를 수행합니다.

이 부분은 매우 직관적이고 편리합니다. 하지만 대량의 데이터를 update 혹은 delete 하는 경우 모든 Entity에 대해서 변경사항을 감지해야 하기 때문에 이 과정 자체는 시간 소요가 꽤 있습니다. 이때, QueryDSL 을 활용하면 성능을 월등하게 개선할 수 있습니다.

 

하지만 주의점이 있습니다.

@Transactional 내부에서 실행되어야 합니다.

영속성 컨텍스트에 있는 Entity 와 무관하게 실행되기 때문에, 쿼리 실행 후 JPA의 영속성 컨텍스트와 DB의 상태가 서로 다르게 됩니다. 쿼리 실행 후 em.clear() 를 통해 영속성 컨텍스트를 초기화해주어 안전하게 사용할 수 있습니다,