본문 바로가기
내일배움캠프/내일배움캠프

[내일배움캠프] TIL : Projection

by TIP__ 2024. 11. 13.

Projection

프로젝션은 JPA로 조회 시 원하는 컬럼을 가져오는 방법입니다.

보통 Spring Data JPA에서 사용하는 방식과 QueryDSL에서 사용하는 방식으로 나뉩니다.

 

프로젝션은 데이터 최적화 및 간결한 데이터 구조를 제공해 성능 향상과 유지보수성에서 큰 장점이 있지만, 수정이 불가능한 읽기 전용 구조와 프로젝션 사용이 많아질수록 복잡해지는 쿼리 관리라는 단점이 있습니다.

 

Base : Store.java

@Entity
@Table(name = "store")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 20, unique = true, nullable = false)
    private String name;
    @Column(columnDefinition = "TEXT")
    private String description;

 


Spring Data JPA

 

1. Interface 기반 Projections

인터페이스를 정의하여 특정 필드만 가져오는 방법입니다.
여기에는 Closed ProjectionsOpen Projections 두 가지 방법이 있습니다.

 
1-1. Closed Projections

Closed Projections는 인터페이스를 통해 반환할 필드를 미리 정의합니다.
각 메서드는 엔티티의 필드 이름과 일치하며, 이 메서드를 통해 값을 가져옵니다.

 

Store 에서 id와 name만 가져오기 위해 interface를 만들고 repository에서 해당 interface로 담는 메서드를 만듭니다.

 

Projection

public interface StoreProjection {
        Long getId();
        String getName();
}

 

Repository

public interface StoreRepository extends JpaRepository<Store, Long> {
    List<StoreProjection> findById(Long id);
}

1-2. Open Projections

Open Projections는 Spring의 SpEL(Expression Language)를 사용하여 데이터를 조합할 수 있습니다.
#{}를 사용하여 필드를 가공하거나 특정 표현식을 사용할 수 있습니다.
테이터 가공을 위한 추가 로직이 필요한 경우에 유용하지만 모든 column을 가져와 처리하기 때문에 성능에 영향을 줄 수 있습니다.

public interface StoreProjection {
        Long getId();
        String getName();

        @Value("#{target.id + ' ' + target.name}")
    String getIdAndName();
}

 

다른 사용방법

public interface StoreProjection {
        Long getId();
        String getName();

    Default getIdAndName();
        return getId() + " " + getName();
}

2. Class 기반 Projections

결과를 특정 클래스의 인스턴스로 반환하는 방법입니다.
DTO와 비슷한 구조로 사용되며 엔티티 필드의 일부 또는 계산된 필드를 포함할 수 있습니다.

 

-. Class 기반 Projection을 사용하려면 반환하려는 Class에 해당하는 생성자가 필요합니다.

-. 생성자는 Projection에서 선택한 필드를 매개변수로 받아야 합니다.

-. Repository에서 Query 결과를 해당 Class의 객체로 매핑할 수 있습니다.

-. 이 방식을 사용하면 interface 기반보다 더 명확한 객체를 반환할 수 있기 때문에 DATA 조작과 유지 및 보수에 유리할 수 있습니다.

public class StoreProjection {
    private Long id;
       private String name; 

public StoreProjection(Long Id, String name) {
    this.id = id;
    this.name = name;
}

    public Long getId() {
    return this.id;
    }

    public String getName() {
    return this.name;
    }
}

 

 

3. Dynamic Projections

메서드를 호출할 때 원하는 Projection 타입을 동적으로 지정하는 방식입니다.
이 방법을 사용하면 여러 Projection을 하나의 Repository 메서드로 처리할 수 있습니다.
예를 들어 id로 조회하는데 어느 부분에서는 id와 name만 필요하고 다른 부분에서는 name과 description만 필요하다면

 

Projection

 public interface StoreIdAndNameProjection {
     Long getId();
    String getName();
 }

 public interface StoreNameAndDescriptionProjection {
    String getName();
    String getDescription();
 }

 

Repository

 public interface StoreRepository extends JpaRepository<Store, Long> {
    <T> T findById(Long Id, Class<T> type);
}

 


QueryDSL

@AllArgsConstructor
public class StoreDto {
    private Long id;
    private String name;
    private String description
}

 

1. Projections.fields()

필드 접근 방식은 DTO에 필드로 매핑하는 방식입니다.
DTO의 필드가 직접 매핑되어있어야 하고 필드 이름이 엔티티의 필드와 동일해야 합니다.

QStore store = Qstore;

List<StoreDto> storeList = queryFactory
        .select(Projections.fields(StoreDto.class,
                Store.id,
                Store.name
        ))
        .from(store)
        .fetch();

 

2. Projections.bean()

Bean 접근 방식은 DTO의 Setter 메서드를 이용해 필드를 설정하는 방식입니다.

QStore store = Qstore;

List<StoreDto> storeList = queryFactory
        .select(Projections.bean(StoreDto.class,
                Store.id,
                Store.name
        ))
        .from(store)
        .fetch();

 

3. Projection.constructor()

생성자 접근 방식은 DTO의 생성자를 이용해 필드를 설정합니다.
DTO에 적절한 생성자가 있어야 하며, 생성자 인자와 매핑할 필드의 순서가 일치해야 합니다.
생성자 접근 방식은 DTO의 모든 필드를 가져와야 합니다.

QStore store = Qstore;

List<StoreDto> storeList = queryFactory
        .select(Projections.constructor(StoreDto.class,
                Store.id,
                Store.name,
                Store.description
        ))
        .from(store)
        .fetch();

 

4. 별칭을 사용하여 필드 매핑

QueryDSL에서 프로젝션 필드 이름이 DTO와 일치하지 않는 경우 Expressionsas()를 사용하여 별칭을 지정해 매핑할 수 있습니다.


as() : 엔티티 필드를 DTO 필드명에 맞게 별칭으로 매핑
Expressions : 복잡한 표현식을 사용할 때 사용되며 아래 예제에서는 두 필드를 합치는 데 사용

 

예를 들어 DTO에서 name이 아니라 private String storeName;private String fullName;으로 되어있는 경우

QStore store = Qstore;

List<StoreDto> storeList = queryFactory
        .select(Projections.constructor(StoreDto.class,
                Store.id,
                Store.name.as("storeName"),
                Expressions.stringTemplate("function('concat',
                                                    {0},
                                                    {1},
                                                    Store.firstName,
                                                    Store.lastName) as ("fullName")
        ))
        .from(store)
        .fetch();

 

5. 동적 프로젝션

메서드 호출 시 원하는 프로젝션을 동적으로 지정할 수 있습니다.

public <T> List<T> findAllStores(Class<T> type) {
    QStore store = Qstore;

    return queryFactory
        .select(Projections.bean(StoreDto.class,
                Store.id,
                Store.name
        ))
        .from(store)
        .fetch();
}

댓글