본문 바로가기

Spring Framework

QueryDSL 바로 알기

반응형

0. 목차 

  1. 왜 ORM인가? 
  2. Spring ORM 기술
  3. QueryDsl 잘. 쓰기
  4. 정리 

1. 왜 ORM인가? 

ORM(Object-Relational Mapping)은 객체와 관계형 데이터베이스 간의 설정을 의미합니다.  

 

ORM은 서버에서 반드시 필요한 데이터베이스 작업 요청의 인터페이스 역할을 담당합니다. 예를 들어 사용자(User)를 조회하는 DB작업이 필요하다면 네이티브 쿼리를 사용하는 경우는 다음 코드를 작성할 것이라고 예상할 수 있습니다. 

SELECT * FROM users WHERE username = [username]

 

 

ORM이 등장하기 이전에는 코드에 DB 종속적인 sql 쿼리가 작성되는 것이 일반적이었고 Spring의 경우도 Mybatis를 써서 코드를 설계하는 경우가 많았습니다. 그럼 만약 현재 사용 중인 DB가 MySQL인데, Oracle이나 MSSQL로 변경되어야 한다고 가정해 봅시다. 코드에 작성된 모든 MySQL 코드를 변경할 DB의 쿼리 문법에 맞게 모두 수정해야 하는 작업이 필요할 것입니다. 매우 번거롭고 귀찮은 일입니다. 

 

ORM의 편리함을 예상할 수 있는 두번째 상황은 안정성입니다. MyBatis를 사용하는 경우 직접 실행해 보기 이전에는 해당 코드가 오류가 있는지 알기 힘듭니다. MyBatis에 작성된 쿼리는 String 타입으로 정의되는 것이 일반적이기 때문에 직접 실행하기 전에는 해당 쿼리의 문법이 올바른지 알기 힘들다는 단점이 있습니다. 

 

ORM이 등장한 이유는 앞서 설명한 것 처럼 데이터베이스에 종속적인 코드 작성을 없애고 객체지향 프로그래밍을 실현하기 위해서 등장했습니다. 

 

ORM은 DB의 테이블을 모두 객체화 하고 객체들 간에 관계를 맺음으로써 관계형 데이터베이스를 추상화시키는 방법입니다. Java는 JPA, PHP는 Eloquent 등 언어마다 ORM 기술들이 다양하지만 근본은 관계형 데이터베이스를 추상화입니다. 


2. Spring ORM 기술

Spring에서는 ORM을 지원하는 대표적인 기술은 다음과 같습니다. 

  • JPA Interface
  • JPQL
  • QueryDSL

JPA Interface는 Spring Data JPA에서 제공하는 JpaRepository 인터페이스를 상속받아 Repository를 직접 구현하도록 지원합니다. 

일반적으로 JPA에서는 객체를 위한 클래스를 생성하고 해당 객체의 생성, 수정, 삭제, 조회를 위한 Repository를 생성하는데, 이때 JpaRepository를 상속받게 되면 Spring Data JPA가 지원하는 기본 메서드들을 사용할 수 있습니다. 

@Repository
public interface UserRepository extends JpaRepository<User, Long> {}

 

예를 들어 객체의 id(일반적으로 pk)에 해당하는 객체를 조회하는 매서드인 findById()와 같은 메서드들을 지원합니다. 

userRepository.findById(id).orElseThrow(EntityNotFoundException::new);

 

 

JPA Interface의 장단점으로는 다음과 같습니다. 

  • 장점: 간단한 CRUD 구현의 생산성을 높여줍니다. 
  • 단점: 복잡한 쿼리를 제한 제한되며, 동적인 쿼리 작성은 어렵거나 가독성이 떨어질 수 있습니다.  

앞서 보여드린 findById같은 메서드를 활용한다면 간단한 CRUD 구현하는 것은 효과적이지만, 복잡한 쿼리(예를 들면 조인절, 서브쿼리, 집계합수 등)의 사용은 제한 적일 수 있습니다. 

 

JPQL(Java Persistence Query Language)는 JPA에서 사용하는 객체 지향 쿼리 언어로 SQL과 유사한 문법으로 엔티티 객체를 대상으로 한 쿼리 작성을 지원하는 기술입니다. 이전에 JPA Interface의 한계를 극복하는 좋은 방법으로 사용될 수 있습니다. 

@Repository
public class UserRepository extends JpaRepository<User, Long> {
	@Query("SELECT u from User u where u.email = :email")
	Optional<User> findByEmail(@Param("email") String email);
}

 

다만 JPQL역시 한계가 있습니다. 쿼리를 직접 작성할 수 있어 유연하지만, 복잡한 조인이나, 집계함수를 작성하는 것은 여전히 제한적이며 문법에 맞게 구현했다고 하더라도 가독성이 매우 떨어지는 코드가 작성될 수 있다는 점입니다. 또한 Mybatis를 썼던 것과 비슷하게 직접 작성된 메서드를 호출하기 전까지는 문법의 오류를 발견하기 힘든데 그 이유는 @Query 어노테이션 내의 쿼리가 문자열로 작성되어 있기 때문입니다. 

 

QueryDSL은 이 모든 한계를 극복하기 위해 등장한 기술입니다. 

@Repository
public class UserRepository {
    @PersistenceContext
    private EntityManager entityManager;

    public List<Product> findProductsByUsername(String username) {
        JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
        
        QUser user = QUser.user;
        QOrder order = QOrder.order;
        QOrderDetail orderDetail = QOrderDetail.orderDetail;
        QProduct product = QProduct.product;

        List<Product> products = queryFactory.select(product)
                .from(user)
                .innerJoin(user.orders, order)
                .innerJoin(order.orderDetails, orderDetail)
                .innerJoin(orderDetail.product, product)
                .where(user.username.eq(username))
                .fetch();

        return products;
    }
}

 

만약 이 코드를 쿼리로 변환한다면 아래와 같을 것인데 이 쿼리가 하드 코딩 되어있다면 가독성도 떨어지고, 안전하지 못한 코드가 될 것입니다. 무엇보다 DB 종속적인 코드로 작성되어 객체지향 프로그래밍의 목적에도 위배될 것이라고 생각합니다. 

SELECT p.*
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN order_details od ON o.id = od.order_id
INNER JOIN products p ON od.product_id = p.id
WHERE u.username = :username

 

QueryDSL은 컴파일 시점에 오류를 발견할 수 있어 쿼리 작성 중 발생하는 오타나 잘못된 필드 접근 등의 오류를 컴파일 시점에서 발견할 수 있기 때문에 안정적입니다. 또한 가독성이 좋기에 readable한 코드를 작성하는데 효과적이라고 생각합니다. 

 

물론 장점만 있다고 생각하지는 않는데, JPA Interface, JPQL에 비해 설정이 복잡하고, 학습 난이도가 높으며, JPQL이 지원하는 기능을 완전히 지원하지 않을 수 있다는 단점이 있습니다. 


3. QueryDsl 잘. 쓰기

Entity를 사용해서 조회하는 경우에는 다음과 같은 문제들이 발생할 가능성이 있습니다. 

  • 불필요한 칼럼이 함께 조회된다. 
  • N+1 문제

정리하자면 Entity를 사용해서 조회하는 경우에는 성능상 문제가 발생할 수 있습니다. 이 문제를 해결할 수 있는 간단한 방법은 DTO(Data Transfer Object)를 활용하는 것입니다. 데이터베이스 쿼리의 결과를 그대로 전달하는 대신, DTO를 사용하여 필요한 필드만 포함해서 데이터를 전달할 수 있습니다. 


DTO 조회는 한 번의 쿼리로 모든 필요한 데이터를 가져올 수 있기 때문에 N+1 문제를 해결할 수 있습니다. 아래는 그 예시 입니다. 

 

@Entity
public class User {
    @Id
    private Long id;
    
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Post> posts;
}

@Entity
public class Post {
    @Id
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
    
    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
    private List<Comment> comments;
}

@Entity
public class Comment {
    @Id
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;
}

 

이 예시에서는 하나의 엔티티인 User가 여러개의 Post를 가지고 있고 각 Post에는 Comment가 여러 개 연결되어 있다고 가정할 때 User를 조회하게 되면 Post, Comment 각각 N번씩 조회되는 N+1문제가 발생할 수 있습니다. 이는 조회 성능을 떨어뜨리는 대표적인 예시입니다. 

 

public class UserDTO {
    private Long id;
    private List<PostDTO> posts;
    
    // Getters and setters
}

public class PostDTO {
    private Long id;
    private List<CommentDTO> comments;
    
    // Getters and setters
}

public class CommentDTO {
    private Long id;
    
    // Getters and setters
}

 

QueryDsl과 DTO를 함께 사용해서 N+1 문제를 해결할 수 있습니다. 해결이 가능한 이유는 앞서 설명한것처럼 한 번의 쿼리로 필요한 모든 데이터를 조회하는 것이 가능하기 때문입니. 또한 DTO를 사용함으로써 필요한 칼럼을 지정해서 조회할 수 있다기 때문에 대용량의 데이터를 조회하는 상황에서 유용하게 사용할 수 있습니다. 

QUser user = QUser.user;
List<UserDTO> userDTOs = new JPAQuery<>(entityManager)
    .select(Projections.constructor(UserDTO.class, user.id, 
             Projections.bean(PostDTO.class, user.posts)))
    .from(user)
    .fetch();

 

다만, 그렇다고 해서 항상 QueryDsl + DTO가 항상 최적인것은 아닙니다. 실시간으로 Entity의 변경이 필요한 경우는 DTO의 사용이 번거로울 수 있습니. 예를 들어 특정 엔티티를 조회하고 그것을 실시간으로 업데이터 해야 하는 경우는 Entity를 사용해서 조회하는 것이 효과적일 수 있습니다. 


4. 정리 

  • JPA Interface는 복잡한 쿼리가 필요없는 CRUD 작업이 필요할 때 유용하다.
  • JPQL은 동적인 쿼리보다는 정적인 쿼리 작성에 더 초점을 두는 상황에서 유용하다.
  • QueryDsl은 타입 안정성을 고려하는 경우나 복잡한 동적 쿼리를 다뤄야 할 때 유용하다.
  • QueryDsl + DTO는 대량의 데이터를 조회하는 경우 QueryDsl + Entity 조회는 실시간으로 Entity 변경이 필요한 경우 유용하다.  
반응형

'Spring Framework' 카테고리의 다른 글

[Spring] spring에 mybatis 적용하기  (0) 2022.11.26
[Spring] PSA(Portable Service Abstraction)  (1) 2022.11.05
[Spring] 예외를 처리하는 방법  (0) 2022.11.01
[Spring] JdbcTemplate  (0) 2022.10.22
[Spring] TDD란?  (0) 2022.10.14