회사 프로젝트를 스프린트에 맞춰서 빠르게 진행하다보니, 프로젝트 구조 및 성능이 미흡한 부분이 많다고 생각했습니다. 요번 스프린트에서는 간단한 기능 추가 및 구조,성능의 부족한 부분에 대해서 공부하고, 방향성에 대해 생각하는 기간이라고 생각합니다. 이번 글에서는 QueryDSL를 이용한 쿼리 최적화 및 DTO 부분에 대한 고찰을 다루고자 합니다.
[목차]
- DTO (Data Transfer Object) 사용범위
- QueryDSL 와 FetchJoin 이용한 쿼리 최적화 및 가독성 증가
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의 장점을 조금 더 공감할 수 있을 거라 생각합니다.
저희 프로젝트에서는, @OneToMany의 양방향 (Collection) 로직이 존재하지 않기 때문에 조금 더 최적화하기 간단하였습니다.
순서를 나열하며 부가 설명을 해보도록 하겠습니다.
1 @ManyToOne, @OneToOne 관계에 있는, 개체간의 연관관계의 fetchType을 전부 Lazy로 걸어두었습니다.
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을 하였습니다. 하나의 쿼리를 통해 데이터를 다 끌어올 수 있는 장점이 있었습니다.
3 fetch join 과 QueryDsl을 통해서 필요한 데이터를 쉽게 가져올 수 있었고, 코드 가독성 증가를 시킬 수 있엇습니다.
많은 내용을 김영한님의 강의를 통해 학습하고, 적용시켰습니다.
(좋은 강의 감사드립니다)
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