데이터베이스/Querydsl

QueryDSL 사용 이유와 방법 ①

수달하나 2023. 2. 24. 10:05

실무에서

개발자를 하기위해 공부를 하던 시절에는 데이터베이스 라는 것에 대해 깊이 생각하지 않았다. 컴퓨터 프로그래밍 기술이 계속적으로 발전 하고 개발자가 좀 더 편하게 개발할 수 있도록 도와주는 프레임워크나 라이브러리를 통해서 많은 부분들에 대해서도 생각할 필요가 없어졌고 마찬가지로 JAVA Spring 을 사용하면서 비지니스 로직에만 몰두하는 개발자가 되어 JPA를 어떻게 잘 사용하면 될 까, 혹은 어떤 라이브러리를 써야지 SQL에 의존적이지 않은 개발을 할 수 있을까만 생각을 했던것 같다. 

뭔가 점점 바보가 되어가는 느낌이다.

운 좋게 면접관으로 여러번 참여할 수 있는 기회를 얻게 되서 다양한 면접자들의 DB 접근 기술을 확인 할 수 있는데 많은 분들이 QueryDSL을 사용해서 쿼리문을 작성하는 것을 확인했다. 근데 막상 왜  QueryDSL을 사용했는지 물어보면 정확하게 대답하지 못하거나 혹은 동적으로 SQL문을 생성할 수 있다는 장점이 있다는 얘기를 한다. 하지만 좀 더 깊은 질문으로 들어가보면 동적 생성 이라는 정확한 의미를 잘 알고 있지는 않은듯 했다. 우선 QueryDSL 에 대해 설명하기 전에 좀 더 기본적인 얘기를 해보자


JPA vs QueryDSL

Java Spring 에서 DB 에 접근한다는 것은?

 

 

JPA(Java Persistence API) 를 통해 사용하다보니 우리는 쿼리문을 직접 날려 Table 을 만들 필요도 없고 CRUD를 통한 기능을 직접 구현해서 사용할 필요가 없어졌다. 그러다 보니 SQL에 비 의존적인 개발자가 되어 가고 있는데 실무에서는 성능 이슈가 발생해서 직접 쿼리문을 작성해야 할 경우도 있다. 따라서 우리는 Java 에서 어떤 방식으로 DB에 접근하는지를 꼭 알아야 한다. JPA는 객체와 RDB의 매핑을 처리하는 표준 API 로 ORM(Object-Relatinal-Mapping) 기술을 사용하여 객체와 테이블을 매핑할수 있도록 하는 기술이다. 

기본적으로 DB에 접근하기 위해서는 JDBC(Java DataBase Connectivity)를 통해 접근을 해야 한다. JDBC를 통해 실제로 Statement를 생성, SQL을 실행해 결과 값을 받아오는데 JPA를 사용하게 되면 이러한 작업을 EntityManager 로부터 대신 수행 할 수있도록 하는 것이다.

그럼 QueryDSL도 같은 역할을 할 까?

같은 역할을 하는 부분도 있지만 사실 두 기술은 존재의 목적이 너무나 다르다.


JPA 와 QueryDSL

QueryDSL 은 Java의 언어를 통해 쉽게 SQL문을 생성하고 JDBC에 대신 접근해 결과 값을 받아온다. 그럼 JPA와 같은 기능을 한다고 생각할 수 있지만 QueryDSL과 JPA의 핵심적인 차이는 ORM기술을 이용한 객체의 관리에 있다. 

위에서 JPA의 기능은 JDBC에 직접 접근하는 것을 얘기했지만 사실 핵심은 객체와 DB의 Mapping 그리고 영속성 관리에 있다. QueryDSL은 SQL을 더 쉽게 작성하기 위한 라이브러리이므로, 객체의 영속성 관리(Persistence)를 위한 ORM(Object-Relational Mapping) 기술이 아니라, SQL을 안전하게 작성하고 쉽게 관리하기 위한 라이브러리로 객체와 데이터베이스 간의 매핑을 위한 Annotation을 사용하지 않을 뿐 더러  ORM 기술에서 제공하는 Entity나 Mapping 과 관련된 기능을 보유하고 있지 않다. 

따라서 QuerlDSL을 통해 받아온 객체의 영속성 관리를 JPA의 EntityManager가 수행 하게 하는것이 일반적으로 우리가 사용했던 JPA와 QueryDSL을 이용하여 객체와 DB의 Mapping을 관리 하는 방법이었다.

그럼 우리가 얘기했던 동적으로 SQL을 생성할 수 있다는 것이 정확히 무엇일까?

 


동적 쿼리의 QueryDSL 

JPA와 QueryDSL을 사용하지 않더라도 자바에서는 JPA Criteria 를 이용해서 마찬가지로 동적으로 SQL을 생성 할 수있다. 따라서 동적으로 생성 할 수 있는 쿼리문은 QueryDSL 만의 장점이 아니다.

하지만 정확하게 설명하자면 가독성이 좋은 동적 쿼리는 QueryDSL의 장점이 될 수 있다. 

// JPA Criteria 

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Employee> query = cb.createQuery(Employee.class);
Root<Employee> root = query.from(Employee.class);

query.select(root).where(cb.equal(root.get("department"), "Sales"));
List<Employee> employees = em.createQuery(query).getResultList();

위 코드에서는 CriteriaBuilderCriteriaQuery 인터페이스를 사용하여 쿼리를 작성하는데 CriteriaQuery 인터페이스의 select 메서드를 사용하여 쿼리에 반환할 엔티티를 지정하고, where 메서드를 사용하여 조건을 추가한다. 다음은 QuerlDSL을 통해서 같은 쿼리를 작성한 예제이다.

// QueryDSL

JPAQuery<Employee> query = new JPAQuery<>(em);
QEmployee employee = QEmployee.employee;

List<Employee> employees = query.selectFrom(employee)
                                .where(employee.department.eq("Sales"))
                                .fetch();

간결한 코드를 통해서 쿼리문을 작성해서 그런지 큰 차이를 보이는 것 같지는 않지만 QueryDSL 을 통해 작성한 코드가 이해 하기 더 편하다는 것을 알 수 있다.  그럼 좀 더 복잡한 코드 예제를 살펴보자.

// JPA Criteria

CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Orders> query = builder.createQuery(Orders.class);
Root<Orders> root = query.from(Orders.class);
Join<Orders, OrderDetails> join = root.join("orderDetails");

List<Predicate> predicates = new ArrayList<>();
predicates.add(builder.equal(root.get("customerName"), "John Doe"));
predicates.add(builder.like(join.get("product"), "%iPhone%"));
query.where(builder.and(predicates.toArray(new Predicate[predicates.size()])));

TypedQuery<Orders> typedQuery = em.createQuery(query);
List<Orders> orders = typedQuery.getResultList();
// QueryDSL

JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QOrders orders = QOrders.orders;
QOrderDetails orderDetails = QOrderDetails.orderDetails;

List<Orders> result = queryFactory.selectFrom(orders)
        .join(orders.orderDetails, orderDetails)
        .where(orders.customerName.eq("John Doe"), orderDetails.product.like("%iPhone%"))
        .fetch();

위 두 예제는 같은 결과 값을 반환하는 코드임에도 불구하고 QueryDSL은 깔끔하고 정리된 직관적인 모습을 보여주며 높은 표현력을 가지고 있는 반면 JPA Criteria 로 작성한 코드는 매우 복잡하다는 것을 확인 할 수 있다. 이 뿐만 아니라 QueryDSL의 Q타입 클래스를 사용하여 컴파일 시간에 타입검사를 시행하여 안정성 또한 보장 할 수 있다.

JPA Criteria의 장점은 JPA 의 일부분 이기에 JPA 표준을 따르고 다른 JPA 구현체에서도 사용이 가능하다는 장점과 대부분의 IED에서 지원을 한다는 장점이 있지만 그 외의 모든 부분에서 사용의 의미가 없다. 


QueryDSL 사용 이유

QueryDSL을 왜 사용 하는 겁니까 라는 질문이 들어왔을 때 동적으로 쿼리를 작성 할 수 있는 장점이 있어서 편합니다. 라는 대답은 맞는 말이지만 사실 모두가 알고 있는 당연한 말이다. 왜 쓰냐 라는 것은 본인이 사용을 하면서 이런 부분이 편했기에 큰 장점이 있는 것 같다 라는 의미이지 단순히 특정 라이브러리의 설명을 원한 질문은 아닐 것이다. 개인적으로 나는 QueryDSL을 사용하면서 업데이트 시 동적으로 null 처리를 수행 할 수 있다는 것이 좋았다. 특정메소드에서 업데이트를 실행 할 때 업데이트 될 수 있는 모든 값을 파라미터로 받아오고 파라미터의 null 체크를 통해 null이 아닌 것들만 업데이트 시킴으로써 update 메소드의 중복과 가독성을 동시에 잡을 수 있다는 점이 크게 매력적이었다. 


QueryDSL 예제

@Entity
@Getter
@Setter
public class Member {

    @Id
    @GeneratedValue
    private Long id;
    
    private String username;
    
    private Integer age;
    
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}

@Entity
@Getter
@Setter
public class Order {

    @Id
    @GeneratedValue
    private Long id;

    private LocalDateTime orderDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
}

연관 관계를 매핑하는 방법은 다양하지만 일단 위와 같은 방식으로 연관관계가 매핑되어 있다고 가정해보자.

1. INNER JOIN

JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
QMember member = QMember.member;
QOrder order = QOrder.order;

List<Tuple> result = queryFactory
        .select(member.username, order.orderDate)
        .from(member)
        .innerJoin(member.orders, order)
        .where(member.age.eq(20))
        .fetch();

2. OUTER JOIN

JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
QMember member = QMember.member;
QOrder order = QOrder.order;

List<Tuple> result = jpaQueryFactory.select(member, order)
    .from(member)
    .leftJoin(order).on(member.id.eq(order.memberId))
    .fetch();

Outer Join 의 경우 Left Outer Join과 Right Outer Join 모두 테이블의 위치만 변경한다면 같은 방식으로 작동하기 때문에 두 가지를 모두 살펴 볼 필요없이 한 쪽만 살펴보면 된다.

3. CROSS JOIN

JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
QMember member = QMember.member;
QOrder order = QOrder.order;

List<Tuple> result = queryFactory
    .select(member.username, order.orderDate)
    .from(member)
    .crossJoin(order)
    .where(member.age.gt(20))
    .fetch();

 

가장 기본적으로 사용되는 Join의 예제를 살펴봤다. 

하지만 실제 실무에서 사용할 때 위와같이 간단한 쿼리를 날리는 상황보다는 좀 더 복잡하고 다양한 조건을 통해 원하는 특정 클래스로 받아오거나 혹은 특정 값 만을 받아오는 상황이 많다. 다음 예제에서는 좀 더 복잡하고 다양한 방식으로 쿼리를 날리는 방법에 대해서 살펴보자.