[Spring] 프로젝트 구조 및 쿼리 성능 향상시키기 (2)

한호성·2023년 3월 22일
0

Introduction

회사 프로젝트를 스프린트에 맞춰서 빠르게 진행하다보니, 프로젝트 구조 및 성능이 미흡한 부분이 많다고 생각했습니다. 요번 스프린트에서는 간단한 기능 추가 및 구조,성능의 부족한 부분에 대해서 공부하고, 방향성에 대해 생각하는 기간이라고 생각합니다. 이번 글에서는 QueryDSL를 이용한 쿼리 최적화 및 DTO 부분에 대한 고찰을 다루고자 합니다.
[목차]

  • DTO (Data Transfer Object) 사용범위
  • QueryDSL 와 FetchJoin 이용한 쿼리 최적화 및 가독성 증가

QueryDSL 와 FetchJoin 이용한 쿼리 최적화 및 가독성 증가

QueryDsl 이란?

QueryDsl이란 JPQL을 코드로 작성할 수 있도록 해주는 프레임 워크입니다.

예를들어, 다음과 같은 JPQL 코드가 있다고 생각해봅시다. 2가지의 @ManyToOne의 관계에 있는 createUser, updateUser를 fetch join을 통해 데이터를 가져옵니다.

    @Query("select c from Contact c join fetch c.company " +
            "join fetch c.createUser " +
            "join fetch c.updateUser " +
            "where c.id = :id")
    Optional<Contact> findByIdFetchJoin(Long id);

hibernate구현체에서 구현되는 sql은 어떤지 확인해 봅시다.

select
        contact0_.contact_id as contact_1_13_0_,
        company1_.company_id as company_1_12_1_,
        user2_.user_id as user_id1_17_2_,
        user3_.user_id as user_id1_17_3_,
        contact0_.create_date as create_d2_13_0_,
        company1_.create_date as create_d2_12_1_,
        user3_.create_date as create_d2_17_3_,
					(중략)...
    from
        tb_contact contact0_ 
    inner join
        tb_company company1_ 
            on contact0_.company_id=company1_.company_id 
    inner join
        tb_user user2_ 
            on contact0_.create_user_id=user2_.user_id 
    inner join
        tb_user user3_ 
            on contact0_.update_user_id=user3_.user_id 
    where
        contact0_.contact_id=?

inner join으로 나가는 것을 확인할 수 있고, @ManyToOne 관계에 있는 User 객체 정보를 다 들고와서 값을 넣어두게 됩니다.

이런 기능을 하는 JPQL을 코드형식으로 만들 수 있는것이 QueryDsl입니다. QueryDsl코드를 잠깐 보도록 하겠습니다.

   @Override
    public Optional<Contact> getContactWithCompany(Long id) {

        return Optional.ofNullable(
                queryFactory.selectFrom(contact)
                        .leftJoin(contact.company, company).fetchJoin()
                        .leftJoin(contact.createUser, user).fetchJoin()
                        .where(contact.id.eq(id)).fetchOne()
        );
    }

다음과 같이 코드 형식으로 작동하게 됩니다. 코드형식으로 짤 수 있어서, IDE의 도움을 받아, 작성할 수 있고, JPQL의 런타임에서 잡히는 오류들을 컴파일 타임에서 잡을 수 있다는 장점이 있습니다. 또 직관적으로 코딩하기에도 더 편하고 띄어쓰기 같은 것들을 고려하지 않아도 되서 정말 좋다고 생각합니다.

정리하자면 QueryDSL의 장점

  • 컴파일 타임에 오류를 쉽게 잡을 수 있다.
  • 코드 형식으로 코딩하기 때문에 String 으로 작성하는 JPQL 보다 생산성이 뛰어나다

실제로 JPQL을 사용하다가 오류가 나거나 , 런타임에 오류가 나는 것을 보면 QueryDSL의 장점을 조금 더 공감할 수 있을 거라 생각합니다.

QueryDSL & FetchJoin 을 활용한 최적화

저희 프로젝트에서는, @OneToMany의 양방향 (Collection) 로직이 존재하지 않기 때문에 조금 더 최적화하기 간단하였습니다.

순서를 나열하며 부가 설명을 해보도록 하겠습니다.

1 @ManyToOne, @OneToOne 관계에 있는, 개체간의 연관관계의 fetchType을 전부 Lazy로 걸어두었습니다.

  • Eager를 들면 entity를 조회하는 순간 관련되 있는 모든 데이터를 끌고와 n+1 문제를 발생시킨다. 물론 Lazy를 걸어도 마찬가지이지만, 이는 뒤에 설명한 fetch join으로 해결가능한다.

2 연관관계에 있는 데이터들을 불러올 때, 필요한 연관관계만을 fetch join 을 통해 불러와 최적화 시켰습니다. 예시를 하나 들어보겠습니다.

    @Override
    public Optional<Contact> getContactWithCompany(Long id) {

        return Optional.ofNullable(
                queryFactory.selectFrom(contact)
                        .leftJoin(contact.company, company).fetchJoin()
                        .leftJoin(contact.createUser, user).fetchJoin()
                        .where(contact.id.eq(id)).fetchOne()
        );
    }

위의 코드를 보시면, contact(외부 회사 사람의 연락처)를 가져오는 코드인데, 추가적인 정보가, 누가 연락처를 등록했는지 (contact.createUser) , 어느 회사 소속인지 (contact.company)인지를 딱딱 찝어서 fetch join을 하였습니다. 하나의 쿼리를 통해 데이터를 다 끌어올 수 있는 장점이 있었습니다.

  • 양방향의 @OneToMany Collecions을 조회해야할 경우, fetch join을 하면 카타시안 곱 원리에 의해, 데이터가 늘어나서 값을 조회해오게 됩니다. --> paging 처리가 안된다.. (그렇기 때문에 이때에는, batch size 설정을 통해 n+1 --> 1+1 문제로 바꿔서 해결할 수 있습니다.) @oneToMany 는 기본 fetchType이 Lazy입니다~

3 fetch join 과 QueryDsl을 통해서 필요한 데이터를 쉽게 가져올 수 있었고, 코드 가독성 증가를 시킬 수 있엇습니다.

Reference

많은 내용을 김영한님의 강의를 통해 학습하고, 적용시켰습니다.
(좋은 강의 감사드립니다)
https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-API%EA%B0%9C%EB%B0%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94

profile
개발자 지망생입니다.

0개의 댓글