본문 바로가기
개발 일기

JPA를 사용하여 여러 테이블에서 데이터를 가져오는 방법에 대한 고찰(2) - JPA DTO PROJECTION

by 황원용 2023. 7. 26.
728x90
지난 글에서 데이터베이스에서 가져올 때 필요한 데이터만 가져와 그대로 DTO에 매핑하여 응답 데이터를 보내는 방식을 설명하다 말았다. 이 부분을 이야기하기 전에 JPA의 DTO PROJECTION에 대해 간단히 설명하고 시작하겠다.

 

JPA DTO PROJECTION

  • 데이터베이스에서 전체 엔터티를 로드하는 것이 아닌 개발자가 지정한 DTO(데이터 전송 객체)로 필요한 데이터를 검색하고 매핑하는 데 사용하는 기술이다.
    • 필요한 데이터만 검색하여 가져오므로 불필요한 데이터의 이동이 없어 성능을 향상시킬 수 있다.

 

프로젝션(PROJECTION) 방법

생성자를 이용한 프로젝션

  • 필수 필드가 있는 DTO의 인스턴스를 직접 생성한다.
  • 필수 필드는 개발자가 가져오고자 하는 데이터와 매핑할 필드를 말한다.

 

생성자를 이용한 프로젝션 예시

public class ResponseDto {
    private Long id;
    private String name;
    private String value;
    private Date updateTime;

    private PromDto(Long id, String name, String value, Date updateTime) {
        this.id = id;
        this.name = name;
        this.value = value;
        this.updateTime = updateTime;
    }
}
@Repository
public interface ProteinRepository extends JpaRepository<Protein, Long> {

    @Query("SELECT new com.example.dto.ResponseDto(p.id, p.name, p.value, p.update_time) FROM Protein p")
	List<ResponseDto> getProteinsByUserId(Long UserId);
}
  • 위와 같은 방법으로 new 명령어를 jpql 문법에 삽입함으로써 조회할 수 있다.
  • SELECT 구문 안에 패키지명을 포함한 전체 클래스 명을 입력해야 하며
  • 순서와 타입이 일치하는 생성자 메서드를 만들어야 한다.

 

 

인터페이스를 이용한 프로젝션(Spring Data JPA 프로젝션)

  • Sprind Data JPA의 프로젝션을 사용하면 엔터티에서 검색하려는 필드를 나타내는 getter 메서드를 사용하여 사용자 지정 인터페이스를 정의할 수 있다.
  • 이 인터페이스는 원하는 DTO의 청사진 역할을 한다.
  • Spring Data JPA는 자동으로 쿼리를 생성하여 데이터를 가져와 프로젝션 인터페이스에 매핑한다.

 

인터페이스를 사용하는 이유는 크게 3 가지가 있다.

  1. DTO를 사용하는데 필요한 최소한의 코드만 작성할 수 있다. 별도의 클래스를 생성하지 않고 인터페이스로 간단하게 필요한 필드만 선언하면 된다.
  2. Spring Data JPA가 쿼리의 결과를 해당 인터페이스에 자동으로 매핑해준다. 
  3. 인터페이스를 사용하여 컴파일 시에 필드 이름 오타나 타입 불일치 등의 오류를 확인할 수 있다.

 

 

인터페이스를 이용한 프로젝션 예시

@Repository
public interface ProteinRepository extends JpaRepository<Protein, Long> {

	//jpql
    @Query("SELECT p.id as id, p.name as name, p.value as value, p.update_time as updateTime FROM Protein p WHERE p.user_id = :userId")
	List<ResponseDto> getProteinsByUserId1(@Param("userId") Long userId); 
    
    //native query
     @Query(value = "SELECT id, name, value, update_time FROM protein_table WHERE user_id = :userId",
           nativeQuery = true)
	List<ResponseDto> getProteinsByUserId2(@Param("userId") Long userId); 
    
    interface ResponseDto {
    	Long getId();
        String getName();
        String getValue();
        Date getUpdateTime();
    }
}

 

  • 위의 코드에서는 jqpl과 native query를 활용하는 예시를 보여주고 있다.
  • 차이점이라고 하면 jpql의 경우에는 as(별칭)을 붙여 인터페이스의 필드명과 매핑해야지만 제대로 조회 쿼리가 나간다.
  • 반면에 native query의 경우에는 별칭을 붙이지 않아도 자동으로 매핑이 된다.

 

 

요구사항에 대입하여 적용해보기

  • 결론부터 말하면 아예 불가능한 것은 아니지만 실패했다.

 

이유는 크게 두 가지가 있다.

  • 첫 번째, 지금 내가 수정 중인 프로젝트는 신규 프로젝트가 아니다.
  • 따라서 이미 모든 테이블마다 전용 레파지토리와 엔터티 클래스가 존재한다.
  • 각 엔터티와 레파지토리를 이용하는 방법의 경우 각 엔터티를 통해 데이터를 가져와서 작업하기 때문에 기존의 코드를 이용할 수도 있었고 변경 없이 확장으로만 작업이 가능했다.
  • 그러나, 여러 테이블에서 데이터를 한 번에 가져오는 방법은 JpaRepository를 이용할 수가 없다. 별도의 엔터티를 만들거나 기존의 엔터티를 이용해야 하기 때문이다.
  • 기존의 엔터티를 이용한다면 7개의 엔터티 중에 어떤 엔터티를 선택할 것인지도 물음표이다. 그렇다고 데이터를 모아 반환하는 용도의 새로운 엔터티를 만드는 것은 뭔가 깔끔한 해결방법이 아니라고 생각했다.
  • 두 번째 이유는 쿼리 성능에 대한 고민이다.
  • 지난 글을 보면 알 수 있듯이 하나의 테이블에서 하나의 컬럼명과 컬럼 값을 key:value 형식으로 내보내기 때문에 컬럼이 여러 개 있으면 그 개수만큼 응답 데이터가 생성된다. 그렇기 때문에 하나의 개별 레파지토리에서 엔터티를 통해 가져오는 데이터 중에 일부만 사용하는 것이 아니라 대부분 사용한다. 단지 여러 번 나눠 응답할 뿐이다. 결국 DTO로 즉시 매핑하여 보내기 위해서는 UNION ALL을 이용하여 엄청나게 많은 SELECT 쿼리를 만들어야 한다.

 

UNION ALL을 통해 데이터를 조회하는 예시

SELECT id, 'protein' as name, protein as value, update_time as updateTime FROM protein WHERE user_id = :userId
UNION ALL
SELECT id, 'carbohydrate' as name, carbohydrate as value, update_time as updateTime FROM carbohydrate WHERE user_id = :userId
UNION ALL
SELECT id, 'morningWeight' as name, morning_weight as value, update_time as updateTime FROM weight WHERE user_id = :userId
UNION ALL
SELECT id, 'nightWeight' as name, night_weight as value, update_time as updateTime FROM weight WHERE user_id = :userId"
  • 정리해 보자. 만약 어떤 테이블에 컬럼이 10개라면 개별 엔터티를 통해 전체 데이터를 조회한 후 10개의 스테틱 메서드를 만드는 것과 SELECT 쿼리 10개를 만들고 UNION ALL로 묶는 것 중에 무엇이 더 좋은가에 대한 고민이 될 것이다.
  • 나는 전자가 더 깔끔하고 쿼리 비용 측면에서도 효율적이라고 생각했다. 지금과 같이 특수한 상황에서 생각해 보면 말이다.

 

 

 이 태스크를 수행하면서 느낀 점은 애초에 왜 EAV 모델로 만들지만 않아도 대부분 할 필요도 없는 고민이었을 텐데.. 였지만 수년간 여러 명의 손을 거치며 여러 번 수정당한(?) 프로젝트이기 때문에 그 구조를 이해하고 바꿀 수 있는 범위에서 최선을 다해 고민하고 결정을 내리는 것이 중요하기 때문에 과정 속에서 많은 공부가 되었음에 만족한다.

 

 

 

참고

chatGPT

https://thalals.tistory.com/359

728x90