[Querydsl] 조회 최적화 : DTO 형식으로 조회하기

진예·2024년 8월 20일
0

Code

목록 보기
3/3
post-thumbnail

⚠️ 문제

특정 상품과 연관된 리뷰 이미지들을 조회하는 쿼리 최적화

⚙️ Review : ReviewImage = 1 : N 연관관계

@Entity
@Getter
@NoArgsConstructor
public class Review extends BaseEntity {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
    
	@Column(nullable = false, updatable = false)
	private String itemCode;
    
    ...
}

@Entity
@NoArgsConstructor
@Getter
public class ReviewImage {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@ManyToOne(fetch=LAZY)
	@JoinColumn(name = "review_id", nullable = false, updatable = false)
	private Review review;

	...
}

⚙️ ReviewImageListDTO : 최종 반환 형태

@Getter
@AllArgsConstructor
public class ReviewImageListDTO {
	private Long reviewId;
	private Long imageId;
	private String url;
	private String alt;
}	

1. 단순 조인

단순 조인 실행 : N + 1 문제 발생

@Query("select ri  from ReviewImage ri left join ri.review r where r.itemCode = :itemCode")
Slice<ReviewImage> findReviewImages(@Param("itemCode") String itemCode, Pageable page);

결과는 정상적으로 반환되나, 전달된 itemCode에 해당하는 리뷰 이미지들을 조회하는 쿼리 하나와, 해당 이미지와 연관된 리뷰 아이디를 조회해오는 2개의 쿼리, 총 3개의 쿼리가 실행되었다.

ReviewImageReview지연 로딩이 적용되어 있기 때문에 이미지와 연관된 리뷰에 대한 정보는 조회하지 않고, 빈 객체인 프록시 객체인 상태로 존재한다. 이 때, 서비스에서 조회 결과를 DTO 형태로 변환하는 과정에서 reviewImage.getReview().getId() 를 통해 연관된 리뷰의 아이디 값을 가져오는데, 이 때 review는 프록시이므로 getId()를 통해 값을 가져오려고 할 때가 되어서야 해당 아이디 값을 가지는 리뷰를 조회하는 쿼리를 실행한다.

이 때, 같은 리뷰 아이디값을 가진다면 영속성 컨텍스트를 활용하여 한 번의 쿼리로 해결할 수 있지만, 연관된 리뷰의 아이디가 n개라면 n개의 추가 쿼리가 발생하게 되면서 성능을 저하시킬 수 있다.


2. Fetch Join

JPA에서 제공하는 페치 조인 실행 : 필요없는 데이터까지 조회하게 됨

@Query("select ri  from ReviewImage ri left join fetch ri.review r where r.itemCode = :itemCode")
Slice<ReviewImage> findReviewImages(@Param("itemCode") String itemCode, Pageable page);

하나의 쿼리로 원하는 결과를 얻을 수는 있지만, 페치 조인연관된 엔티티들, 즉 리뷰와 리뷰 이미지 테이블 내의 모든 컬럼을 한 번에 조회하게 된다. 저기서 실질적으로 4개의 필드만 사용하고 나머지 필드들은 사용하지 않는데, 4개의 데이터를 가져오려고 9개의 데이터까지 덤으로 매번 가져오는 건 성능적으로 비효율적이지 않을까? 생각이 들었다.. 사실 덩치 큰 쿼리가 마음에 안들었음 ㅎ..


💡 해결책

엔티티 타입으로 조회해서 DTO로 변환하는 방식이 아닌, DTO 타입에 맞춰서 조회하기!


1. JPQL

select new com.comehere.ssgserver.review.dto.resp.ReviewImageListDTO...

: JPQL에서 DTO 타입으로 조회하기 위해서는 조회 타입에 조회하고 싶은 형식의 DTO생성자 선언하듯이 선언하면 되는데, DTO가 존재하는 경로 상의 모든 패키지명을 명시해야 하므로 쿼리가 지저분해진다.. 따라서 해당 방식은 패스..


2. Querydsl

QuerydslProjections 사용 : 생성자, 필드, 어노테이션 등 다양한 방법이 있는데 나는 생성자 주입 방식 사용!

  • Projections.constructor : 생성자(@AllArgsConstructor)를 통해 조회된 값을 DTO 타입에 주입
List<ReviewImageListDTO> result = query
				.select(Projections.constructor(ReviewImageListDTO.class,
						reviewImage.review.id.as("reviewId"),
						reviewImage.id.as("imageId"),
						reviewImage.imageUrl.as("url"),
						reviewImage.alt))
				.from(reviewImage)
				.join(reviewImage.review, review)
				.where(review.itemCode.eq(itemCode))
				.limit(page.getPageSize() + 1)
				.offset(page.getOffset())
				.fetch();

하나의 쿼리필요한 데이터만 조회 완.

profile
백엔드 개발자👩🏻‍💻가 되고 싶다

0개의 댓글