카테고리 없음

[SERVER] - EducationClassProject : Query dsl로 클래스 필터링 기능 구현

Kim David 2024. 9. 21. 14:37

query DSL(domain specific language)

 

하이버네이트 쿼리 언어(HQL)의 쿼리를 타입에 안전하게 생성 및 관리해주는 프레임워크입니다.

정적 타입을 이용하여 SQL과 같은 쿼리를 생성할 수 있게 해줍니다.

 

기존 객체지향 어플리케이션과 관계형 db 불일치 문제를 해결해주는 것이 jpa 프레임 워크입니다.

따라서 개발자는 객체지향 관점에서 개발을 하고, jpa 프레임워크는 자동으로 sql 쿼리문을 생성하여 진행 할 수 있었습니다. ( sql문이 자동으로 생성되니, 개발자는 sql 관점 프로그래밍을 안해도 됩니다. )

 

하지만 완전한 분리는 불가능 합니다. 따라서 jpa는 jpql을 지원합니다.

jpa가 모든 쿼리를 객체방식으로 표현할 수 없기에 이를 커버하기 위해 jpql을 지원하는 것입니다.

 

jpql의 문제점

하지만 여기에는 문제점이 있습니다.

우선 jpql은 문자열이기에 오류가 발생해도 알아차리지 못한다는 점입니다. @Query 어노테이션을 사용하면 프로그램 실행중 오류를 발생할 수 있지만 이또한 프로그램을 실행시켜야한다는 단점이 있습니다.

즉, jpql을 파싱하는 프로세스가 동작해야지만 문법 오류를 알아차릴 수 있어서 타입 안정성이 떨어집니다.

( 컴파일 과정에서는 오류를 알아챌 수 없습니다. )

 

또한 jqpl은 문자열이기에 동작쿼리를 작성하려면 문자열을 조작하는 방식으로 로직을 구성해야하는데 이때문의 가독성이 매우 흐려집니다. 

 

따라서 이런점들을 보완하기 위해 복잡한 쿼리들은 query DSL로 리팩토링을 거쳐야했습니다.

 

동작 원리

 

 

 

기존 JPQL이 실행되는 원리를 보시면 개발자가 직접 JPQL을 작성하면 타입 안정성을 체크할 수도 없고, 직관적인 동적 쿼리또한 작성하지 못합니다.

 

 

하지만 query dsl에서 jpql이 실행되는 원리를 보시게되면, 개발자는 query dsl이 jpql을 작성할 수 있도록 필요한 데이터를 세팅하여 전달해야합니다. 

그러나 entity는 jpa 프레임 워크에서 지원하는 모듈로써, 쿼리 생성에 특화된 query dsl은 jpa 프레임워크와 분리되어있습니다. 따라서 다른 프레임워크에 모듈을 그대로 쓰지않고 query dsl은 entity 정보를 담은 q타입 클래스를 사용합니다.

( 다른 프레임워크의 모듈을 그대로 사용해버리면 jpa 프레임워크에 종속되어버립니다. )

 

이 q타입 클래스는 query dsl 플러그인으로 컴파일하면 지정된 위치에 생성되게 되는데, 개발자는 q타입 객체를 생성하여 jpql 생성에 필요한 데이터를 query dsl 프로세스에 넘길 수 있습니다.

 

 

현재 진행중인 프로젝트에 대입

 

현재 제가 진행중인 educationclassproject는 springdata jpa를 사용하여 구현하였는데 복잡한 쿼리, 동적 쿼리를 구현하는데 있어서, 한계가 있다고 판단되었습니다. ( 복잡한 쿼리를 구현할때는 jpql을 사용하여 구현하였습니다. )

하지만 jpql로 쿼리문을 작성한 결과 컴파일시 오류를 알 수가 없어서 문제가 되었습니다.

그리하여 query dsl을 도입하게 된다면 자바코드로 sql문을 작성 할 수 있게되어 컴파일시 오류를 발생시켜 잘못된 쿼리가 실행되는 것을 방지 할 수 있다고 생각하였습니다.

 

기존 프로젝트에서는

@Query("select m from Member m where m.username = :username")
    List<Member> findByUsername(@Param("username") String username);

 

위에와 같이 레포지토리에서 @Query 어노테이션을 사용하여 쿼리문을 작성하여 뽑아와야했습니다.

하지만 query dsl을 도입하게 된다면 기존 코드와 다르게 java문을 이용하여 쿼리문을 작성 할 수 있습니다.

String username = "java";

List<Member> result = queryFactory
        .select(member)
        .from(member)
        .where(usernameEq(username))
        .fetch();

 

따라서 복잡한 쿼리나 동적 쿼리 작성이 편리하다는 이점이 있습니다.

또한 쿼리 작성시 제약조건등을 메서드 추출을 통해 재사용 할 수 있다는 이점이 있습니다.

 

  @Query("SELECT o FROM Order o LEFT JOIN FETCH o.payment p LEFT JOIN FETCH o.user m WHERE o.orderUid = :orderUid")
    Optional<Order> findOrderAndPaymentAndMember(@Param("orderUid") String orderUid);

 

해당 쿼리문은 현재 진행중인 프로젝트에서 결제 기능 구현 당시 orderUid를 통해 Order 객체를 가져오는데 성능 최적화를 위해 즉시 로딩으로 order와 관련된 payment와 user을 가져왔습니다.

 

하지만 해당 쿼리문에 query dsl을 적용하게되면,

public class OrderRepositoryImpl implements OrderRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    public OrderRepositoryImpl(JPAQueryFactory queryFactory) {
        this.queryFactory = queryFactory;
    }

    @Override
    public Optional<Order> findOrderAndPaymentAndMember(String orderUid) {
        Order result = queryFactory
            .selectFrom(order)  // QOrder를 사용하여 Order 엔티티를 선택
            .leftJoin(order.payment, payment).fetchJoin() // QPayment를 사용하여 조인
            //order와 payment간의 leftjoin을 해주는데 모든 주문을 가져오되, 해당 주문에 대한 결제가 없더라도 주문 정보를 포함해줍니다.
            .leftJoin(order.user, user).fetchJoin()       // QUser를 사용하여 조인
            // 모든 주문을 가져오되, 해당 주문에 대한 사용자가 없어도 가져와줍니다.
            .where(order.orderUid.eq(orderUid))           // 조건 설정
            .fetchOne();

        return Optional.ofNullable(result);
    }
}

 

복잡한 쿼리문을 좀 더 직관적이게 구현을 할 수 있습니다. ( 유지 보수 용이 )

 

 

 

 

그렇다면 만약 현재 진행중인 프로젝트에서 강의를 필터링할때 이름, 레벨, 시작일에 대해서 필터링을 진행해주고싶다고 하면

import com.querydsl.jpa.impl.JPAQueryFactory;
import static com.yourpackage.domain.QClass.classEntity; // QClass import
import java.time.LocalDate;
import java.util.List;

public class ClassRepositoryImpl implements ClassRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    public ClassRepositoryImpl(JPAQueryFactory queryFactory) {
        this.queryFactory = queryFactory;
    }

    @Override
    public List<Class> searchClasses(String className, String classLevel, LocalDate classStartDay) {
        return queryFactory
            .selectFrom(classEntity)
            .where(classEntity.className.contains(className) // 이름 필터링
                   .and(classEntity.classLevel.eq(classLevel)) // 레벨 필터링
                   .and(classEntity.classStartDay.eq(classStartDay))) // 시작일 필터링
            .fetch();
    }
}

 

해당 코드처럼 where을 통해 조건을 걸어서 뽑아 올 수 있을 것입니다.