https://sundaland.tistory.com/426
[ ▶ Projections ]
Spring Data JPA에서 프로젝션은 엔티티의 전체 데이터가 아니라 특정 속성만 조회할 때 유용한 방법dl다. 특히 대규모 데이터를 다룰 때 불필요한 정보를 조회하지 않고 필요한 속성만을 가져오는 데 도움을 준다.
[ ▷ 기본 개념: 프로젝션이란? ]
일반적으로 Spring Data JPA의 쿼리 메서드는 Repository가 관리하는 엔티티, 즉 Aggregate Root의 전체 객체를 리턴한다. 하지만 일부 속성만 조회하고 싶을 때가 있습니다. 이때, 프로젝션을 사용하여 필요한 속성만 조회할 수 있습니다.
Aggregate Root [ https://blank001.tistory.com/153 ] (수정이 필요함)
[ ▷ 기본 예시: 엔티티와 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는 이를 해결하기 위해 다양한 프로젝션 방식을 제공한다.
[ ▷ 인터페이스 기반 프로젝션(Interface-based Projections) ]
인터페이스 기반 프로젝션은 가장 간단한 방법 중 하나다. 필요한 속성에 대한 getter 메서드를 포함한 인터페이스를 정의하고 이를 반환 타입으로 사용한다.
△ 인터페이스 정의
먼저 이름과 성을 반환하는 NamesOnly 인터페이스를 정의한다.
interface NamesOnly {
String getFirstname();
String getLastname();
}
△ Repository에서 인터페이스 사용
이제 PersonRepository에서 이 인터페이스를 반환하도록 수정한다.
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}
이렇게 정의하면 findByLastname() 메서드가 NamesOnly 인터페이스를 반환하며, 이는 실제로는 Person 객체의 일부 데이터만 조회하는 것이다. Spring Data JPA는 실행 시 해당 인터페이스의 프록시를 생성하고, 인터페이스의 메서드 호출을 Person 객체의 해당 속성에 위임한다.
[ ▷ 재귀적 프로젝션 (Recursive Projection) ]
재귀적 프로젝션을 사용하면 엔티티 내부에 포함된 다른 객체도 프로젝션을 적용할 수 있다. 예를 들어, Person 엔티티의 Address 속성 일부도 조회하고 싶다면, AddressSummary라는 인터페이스를 추가할 수 있다.
△ 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, Address의 City 속성만을 조회할 수 있다.
[ ▷ 클로즈드 프로젝션 (Closed Projections) ]
클로즈드 프로젝션은 프로젝션 인터페이스의 메서드가 모두 엔티티의 실제 속성과 1:1로 매핑되는 경우를 의미한다. 즉, 인터페이스가 리턴하는 모든 속성은 엔티티에 정확하게 존재해야 다. 앞서 정의한 NamesOnly가 그 예시다.
interface NamesOnly {
String getFirstname();
String getLastname();
}
클로즈드 프로젝션은 Spring Data JPA가 쿼리 최적화를 적용할 수 있다는 장점이 있다. 필요한 속성만 쿼리하기 때문에 불필요한 데이터를 조회하지 않게 되어 성능이 향상된다.
[ ▷ 오픈 프로젝션 (Open Projections) ]
오픈 프로젝션은 SpEL (Spring Expression Language)을 사용하여 계산된 값을 반환할 수 있다. 이 경우, 엔티티의 속성뿐만 아니라 커스텀 로직을 통해 새로운 값을 만들어 리턴할 수 있다.
△ SpEL을 사용한 오픈 프로젝션 예시
다음은 SpEL을 사용해 이름과 성을 결합한 전체 이름을 리턴하는 예시다.
interface NamesOnly {
@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();
}
이 방식은 기존 엔티티의 속성(firstname, lastname)을 조합해 새로운 값을 만들어 리턴한다. 하지만 SpEL을 사용하기 때문에, Spring Data JPA가 쿼리 최적화를 적용하지 못한다는 단점이 있다.
△ Default Method를 사용한 오픈 프로젝션
Java 8에서 도입된 Default Methods를 활용하면, SpEL 대신 메서드 자체에서 로직을 처리할 수 있다.
interface NamesOnly {
String getFirstname();
String getLastname();
default String getFullName() {
return getFirstname() + " " + getLastname();
}
}
이 방법은 프로젝션 인터페이스 내에서 간단한 로직을 처리하고자 할 때 유용하다.
[ ▷ DTO 기반 프로젝션 (Class-based Projections) ]
DTO를 사용하여 프로젝션을 정의할 수도 있다. 이 경우 인터페이스 대신 별도의 클래스를 정의하고, 필요한 속성만을 포함할 수 있다.
△ DTO 클래스 정의
public record NamesOnlyDTO(String firstname, String lastname) {}
# record [ https://blank001.tistory.com/154 ] 수정 필요
△ Repository에서 DTO 사용
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnlyDTO> findByLastname(String lastname);
}
이 방법은 엔티티의 전체 속성을 가져오지 않고 필요한 속성만을 대상으로 하는 DTO 객체를 생성하여 반환한다. 또한, Record를 사용하면 불변성, equals(), hashCode(), toString() 메서드가 자동으로 생성되므로 더 간결하게 코드를 작성할 수 있다.
[ ▷ 동적 프로젝션 (Dynamic Projections) ]
동적 프로젝션을 사용하면 실행 시점에 원하는 프로젝션 타입을 선택할 수 있다. 이때는 제네릭 파라미터와 Class 타입을 사용한다.
△ 동적 프로젝션을 위한 Repository 메서드 정의
interface PersonRepository extends Repository<Person, UUID> {
<T> Collection<T> findByLastname(String lastname, Class<T> type);
}
△ 동적 프로젝션 사용 예시
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.25 |
---|---|
Transactionality (0) | 2024.10.25 |
Query Hint (0) | 2024.10.24 |
Scrolling (0) | 2024.10.24 |
Using @Query (0) | 2024.10.24 |