Projections

2024. 10. 22. 18:15Spring Boot/Spring Data JPA

Spring Data JPA에서 프로젝션은 엔티티의 전체 데이터가 아니라 특정 속성만 조회할 때 유용한 방법입니다. 특히 대규모 데이터를 다룰 때 불필요한 정보를 조회하지 않고 필요한 속성만을 가져오는 데 도움을 줍니다. 이번 설명에서는 Spring Data JPA에서 제공하는 프로젝션의 여러 방법에 대해 설명하고 샘플 코드를 통해 이해를 도울 것입니다.

1. 기본 개념: 프로젝션이란?

일반적으로 Spring Data JPA의 쿼리 메서드는 Repository가 관리하는 엔티티, 즉 Aggregate Root의 전체 객체를 리턴합니다. 하지만 일부 속성만 조회하고 싶을 때가 있습니다. 이때, 프로젝션을 사용하여 필요한 속성만 조회할 수 있습니다.

2. 기본 예시: 엔티티와 Repository

먼저, Person이라는 엔티티와 해당 Repository를 정의해 보겠습니다.

class Person {
    @Id UUID id;
    String firstname, lastname;
    Address address;

    static class Address {
        String zipCode, city, street;
    }
}

interface PersonRepository extends Repository<Person, UUID> {
    Collection<Person> findByLastname(String lastname);
}

※ UUID 타입의 id 값을 확인하기 위해서는-예를 들어 MySQL-사용하고 있다면, 워크벤치에서 다음 쿼리를 사용해서 확인할 수 있습니다.

SELECT
    HEX(id) AS hex_value,
    CONCAT(
        SUBSTRING(HEX(id), 1, 8), '-',
        SUBSTRING(HEX(id), 9, 4), '-',
        SUBSTRING(HEX(id), 13, 4), '-',
        SUBSTRING(HEX(id), 17, 4), '-',
        SUBSTRING(HEX(id), 21, 12)
    ) AS formatted_uuid
FROM person;

그리고 클라이언트에서는 다음과 같은 타입으로 보내야 합니다

 {
     "id": "550e8400-e29b-41d4-a716-446655440000"
 }

이 구조에서는 findByLastname(String lastname) 메서드가 Person 객체의 리스트를 반환합니다. 하지만 Person 엔티티의 모든 속성이 필요하지 않고, 이름과 성만 필요하다고 가정해봅시다. Spring Data JPA는 이를 해결하기 위해 다양한 프로젝션 방식을 제공합니다.

3. 인터페이스 기반 프로젝션(Interface-based Projections)

인터페이스 기반 프로젝션은 가장 간단한 방법 중 하나입니다. 필요한 속성에 대한 getter 메서드를 포함한 인터페이스를 정의하고 이를 반환 타입으로 사용합니다.

3.1 인터페이스 정의

먼저 이름과 성을 반환하는 NamesOnly 인터페이스를 정의합니다.

interface NamesOnly {
    String getFirstname();
    String getLastname();
}

3.2 Repository에서 인터페이스 사용

이제 PersonRepository에서 이 인터페이스를 반환하도록 수정해 봅시다.

interface PersonRepository extends Repository<Person, UUID> {
    Collection<NamesOnly> findByLastname(String lastname);
}

이렇게 정의하면 findByLastname() 메서드가 NamesOnly 인터페이스를 반환하며, 이는 실제로는 Person 객체의 일부 데이터만 조회하는 것입니다. Spring Data JPA는 실행 시 해당 인터페이스의 프록시를 생성하고, 인터페이스의 메서드 호출을 Person 객체의 해당 속성에 위임합니다.

4. 재귀적 프로젝션(Recursive Projection)

재귀적 프로젝션을 사용하면 엔티티 내부에 포함된 다른 객체도 프로젝션을 적용할 수 있습니다. 예를 들어, Person 엔티티의 Address 속성 일부도 조회하고 싶다면, AddressSummary라는 인터페이스를 추가할 수 있습니다.

4.1 PersonSummary 인터페이스 정의

interface PersonSummary {
    String getFirstname();
    String getLastname();
    AddressSummary getAddress();

    interface AddressSummary {
        String getCity();
    }
}

이제 PersonSummary를 반환하는 메서드를 Repository에 추가할 수 있습니다.

interface PersonRepository extends Repository<Person, UUID> {
    Collection<PersonSummary> findByLastname(String lastname);
}

이 방식으로 Person 객체에서 Firstname, Lastname, AddressCity 속성만을 조회할 수 있습니다.

5. 클로즈드 프로젝션 (Closed Projections)

클로즈드 프로젝션은 프로젝션 인터페이스의 메서드가 모두 엔티티의 실제 속성과 1:1로 매핑되는 경우를 의미합니다. 즉, 인터페이스가 리턴하는 모든 속성은 엔티티에 정확하게 존재해야 합니다. 앞서 정의한 NamesOnly가 그 예시입니다.

interface NamesOnly {
    String getFirstname();
    String getLastname();
}

클로즈드 프로젝션은 Spring Data JPA가 쿼리 최적화를 적용할 수 있다는 장점이 있습니다. 필요한 속성만 쿼리하기 때문에 불필요한 데이터를 조회하지 않게 되어 성능이 향상됩니다.

6. 오픈 프로젝션 (Open Projections)

오픈 프로젝션은 SpEL (Spring Expression Language)을 사용하여 계산된 값을 반환할 수 있습니다. 이 경우, 엔티티의 속성뿐만 아니라 커스텀 로직을 통해 새로운 값을 만들어 리턴할 수 있습니다.

6.1 SpEL을 사용한 오픈 프로젝션 예시

다음은 SpEL을 사용해 이름과 성을 결합한 전체 이름을 리턴하는 예시입니다.

interface NamesOnly {
    @Value("#{target.firstname + ' ' + target.lastname}")
    String getFullName();
}

이 방식은 기존 엔티티의 속성(firstname, lastname)을 조합해 새로운 값을 만들어 리턴합니다. 하지만 SpEL을 사용하기 때문에, Spring Data JPA가 쿼리 최적화를 적용하지 못한다는 단점이 있습니다.

6.2 Default Method를 사용한 오픈 프로젝션

Java 8에서 도입된 Default Methods를 활용하면, SpEL 대신 메서드 자체에서 로직을 처리할 수 있습니다.

interface NamesOnly {
    String getFirstname();
    String getLastname();

    default String getFullName() {
        return getFirstname() + " " + getLastname();
    }
}

이 방법은 프로젝션 인터페이스 내에서 간단한 로직을 처리하고자 할 때 유용합니다.

7. DTO 기반 프로젝션 (Class-based Projections)

DTO를 사용하여 프로젝션을 정의할 수도 있습니다. 이 경우 인터페이스 대신 별도의 클래스를 정의하고, 필요한 속성만을 포함할 수 있습니다.

7.1 DTO 클래스 정의

public record NamesOnlyDTO(String firstname, String lastname) {}

7.2 Repository에서 DTO 사용

interface PersonRepository extends Repository<Person, UUID> {
    Collection<NamesOnlyDTO> findByLastname(String lastname);
}

이 방법은 엔티티의 전체 속성을 가져오지 않고 필요한 속성만을 대상으로 하는 DTO 객체를 생성하여 반환합니다. 또한, Record를 사용하면 불변성, equals(), hashCode(), toString() 메서드가 자동으로 생성되므로 더 간결하게 코드를 작성할 수 있습니다.

8. 동적 프로젝션 (Dynamic Projections)

동적 프로젝션을 사용하면 실행 시점에 원하는 프로젝션 타입을 선택할 수 있습니다. 이때는 제네릭 파라미터와 Class 타입을 사용합니다.

8.1 동적 프로젝션을 위한 Repository 메서드 정의

interface PersonRepository extends Repository<Person, UUID> {
    <T> Collection<T> findByLastname(String lastname, Class<T> type);
}

8.2 동적 프로젝션 사용 예시

void someMethod(PersonRepository personRepository) {
    Collection<Person> fullResult = personRepository.findByLastname("Matthews", Person.class);
    Collection<NamesOnly> projectionResult = personRepository.findByLastname("Matthews", NamesOnly.class);
}

위와 같이, 호출 시점에 반환할 타입을 지정할 수 있어 더 유연하게 프로젝션을 사용할 수 있습니다.

 

Spring Data JPA의 프로젝션은 엔티티의 일부분만을 선택적으로 조회하고, 필요한 데이터만을 리턴받아 성능을 최적화하는 유용한 기능입니다. 인터페이스 기반 프로젝션, DTO 기반 프로젝션, 그리고 동적 프로젝션 등 다양한 방법을 제공하며, 각 방법은 상황에 따라 유연하게 사용할 수 있습니다.

각각의 프로젝션 방법은 장단점이 있으므로, 요구 사항에 맞게 적절한 방법을 선택하는 것이 중요합니다.

'Spring Boot > Spring Data JPA' 카테고리의 다른 글

Locking  (0) 2024.10.22
Transactionality  (0) 2024.10.22
Scrolling  (0) 2024.10.20
Configuring Fetch- and LoadGraphs  (0) 2024.10.20
Applying Query Hints  (0) 2024.10.20