본문 바로가기
[개발] 프레임워크/QueryDSL

[QueryDSL] projection으로 테이블 조인 결과 데이터 매핑하기

by Devsong26 2024. 12. 3.

MyBatis 코드를 QueryDSL로 이관하는 작업 중에 여러 개의 테이블의 조인되어 있는 결과를 반환하는 매핑 클래스를 만들어야 했습니다. QueryDSL의 경우 projection이라고 해서 SELECT 절의 조회 대상을 지정하는 방법이 있었습니다. projection 용 record 타입의 dto를 하나 만들어 결과를 매핑하여 처리를 하였는데요. 그 코드를 한 번 소개하려고 합니다.

 

 

 

Projection


Projection은 프로그래밍 및 데이터베이스 분야에서 전체 데이터 중 일부만 선택해 가져오는 작업을 말합니다. 주로 QueryDSL, JPA, SQL 등에서 사용되며, 필요한 데이터만 선택적으로 추출하여 효율적인 데이터 전송 및 처리를 가능하게 합니다.

 

Projection 사용 시 주의 사항 

DTO 클래스와 생성자와 필드 매칭을 신경써야 합니다. Projections.construtor를 사용할 경우, DTO의 생성자와 쿼리에서 반환되는 필드 순서가 정확히 일치해야 합니다. DTO의 생성자 필드 순서가 변경되거나 누락되면 런타임 오류가 발생합니다. 컴파일 시점에 검증되지 않아 디버깅이 어려울 수 있습니다.

 

Alias를 사용할 수 있습니다. Projection 결과를 DTO의 필드와 매핑할 때, 필드 이름이 다른 경우 as()를 사용해 매핑해야 합니다. 필드 이름이 다르면 데이터가 제대로 매핑되지 않아 필드가 null로 채워질 수 있습니다.

 

Projection 사용 시 Overfetching을 주의해야 합니다. 필요한 필드만 선택해야 하며 엔티티 전체를 불필요하게 가져오면 Overfetching으로 성능 문제가 발생할 수 있습니다. 모든 필드를 가져오면 네트워크 트래픽 증가 및 메모리가 낭비될 수 있습니다.

 

 💡 Overfetching이란?

데이터베이스 또는 API에서 불필요하게 많은 데이터를 가져오는 문제를 의미합니다. 즉, 애플리케이션이 실제로 필요로 하지 않는 데이터까지 가져와 성능 저하를 초래하는 상황을 말합니다.

 

null 필드 처리를 해야 합니다. 쿼리 결과 중 일부 필드가 null일 가능성이 있는 경우 이를 적절히 처리해야 합니다. null 값이 예상되지 않은 필드로 매핑되면 NPE가 발생합니다. coalesce()를 사용하여 기본값을 설정하여 처리할 수 있습니다.

 

JPA와 연관관계 충돌과 해결 방법

지연 로딩(Lazy Loading)의 문제가 있습니다. JPA에서 @OneToMany, @ManyToOne과 같은 연관관계 필드는 기본적으로 지연 로딩을 설정합니다. Projection을 사용할 경우, QueryDSL은 지정된 필드만 SELECT 쿼리로 가져오지만, 연관관계 필드에 접근하는 시점에 추가 쿼리가 발생할 수 있습니다. 이로 인해 N + 1 문제가 발생할 가능성이 높습니다.

 

데이터 플랫화(Data Flattening)는 필요한 데이터를 한 쿼리에서 평탄화(Flat)하여 반환합니다. 핵심은 데이터를 플랫(flat)한 형태로 반환하여 DTO로 매핑하는 데 중점을 두는 방법으로, 데이터 플랫화는 DTO 중심으로 설계하여 필요한 데이터만 쿼리 결과로 반환하는 데 초점이 맞춰져 있습니다.

 

💡 데이터 플랫화(Data Flattening)란?

복잡한 계층적 데이터 구조를 단순한 형태로 변환하는 과정을 의미합니다. 즉, 다중 테이블, 중첩된 객체, 혹은 계층적으로 연결된 데이터를 단일 평면 구조로 펼쳐서 사용할 수 있도록 변환하는 작업입니다. 데이터베이스, API 응답 설계, 또는 데이터 처리 과정에서 자주 사용됩니다.

 

 

이제 제가 작성했던 코드를 소개하려고 합니다.

 

1. record 타입의 DTO 클래스 생성

public record SongJoinAlbumDTO (
    Long songId, 
    String songName,
    Long albumId,
    String albumName    
) {


}

 

DTO는 로직이 없이 데이터를 프로세스 간에 전달하는 것만을 목적으로 해야 하기 때문에 record 타입으로 생성했습니다.

 

2. QueryDSL에서 projection으로 테이블 조인 결과 매핑하기

import static com.querydsl.core.types.Projections.constructor;
import static [path].QSong.song;
import static [path].QAlbum.album;


@RequiredArgsConstructor
public class SongRepositoryImpl implements SongRepositoryCustom {

    private final JPAQueryFactory jpaQueryFactory;
	
    public List<SongJoinAlbumDTO> findAllBySongIdIn(final List<Long> songIdList) {
    	return jpaQueryFactory.select(
        	constructor(SongJoinAlbumDTO.class,
            	song.id, song.name, album.id, album.name
            )
            .from(song)
            .join(album).on(song.albumId.eq(album.id))
            .where(
            	song.id.in(songIdList)
            )
            .fetch();
        )
    
    }
    
}

 

위의 메서드 구현부는 다음 쿼리를 수행합니다.

SELECT s.id, s.name, a.id, a.name
FROM song s
JOIN album a ON s.album_id = a.id
WHERE s.id IN (...)

 

 

 

Reference


GPT