콘텐츠로 이동

2025 09 30

2025-09-30

Spring Data JPA projections

참고: https://docs.spring.io/spring-data/jpa/reference/repositories/projections.html#projections.interfaces

  • 개요
    • Spring Data 쿼리는 주로 1개/여러개의 인스턴스를 리포지토리에서 반환
    • 특정 프로젝션 (쿼리 결과를 특정한 데이터 구조에 매핑) 이 필요할 때가 있음
  • Interface-based Projections

    • 쿼리 결과를 특정 이름 속성에 매핑하는 가장 쉬운 방법은, 인터페이스를 선언하여 getter를 열어두는 것
      interface PersonRepository extends Repository<Person, UUID> {
          Collection<NamesOnly> findByLastName(String lastName);
      }
      
      interface NamesOnly {
          String getFirstName();
          String getLastName();
      }
      
    • Query Execution Engine에서 런타임에 프록시 인스턴스 만들어서 해당 메서드 쓸 수 있도록 제공
    • 재귀적으로 interface안에 interface 넣어서 정의해도 사용 가능
    • Closed Projections
      • interface의 getter가 타겟 프로퍼티에 꼭 맞으면 Closed Projection이라고 지칭
      • 해당 속성을 이미 알고 있기 때문에, Spring Data가 쿼리 실행 최적화 함.
    • Open Projections

      • @Value를 사용해서 값을 조작해서 들고 올수도 있는 듯
        interface NamesOnly {
            @Value("#{target.firstname + ' ' + target.lastname}")
            String getFullName();
        }
        
      • 해당 값은 너무 복잡하게 쓰지 마시고, interface의 default 메서드를 쓰는 것도 좋은 방법
  • Class-based Projections (DTOs)
    • interface 사용과 완전히 동일한 방식이지만, 프록싱(당연)과 nested projection이 없음
    • Java의 record를 많이 사용
      • 모든 필드 private final, equals/hashCode/toString 선 정의 되어있음
    • 필요하다면, 제네릭을 사용하여 Dynamic Projection도 사용 가능
      interface PersonRepository extends Repository<Person, UUID> {
        <T> Collection<T> findByLastname(String lastname, Class<T> type);
      }
      
      void someMethod(PersonRepository peopleRepository) {
          Collection<Person> aggregates = peopleRepository.findByLastname("Matthews", Person.class);
          Collection<NamesOnly> aggregates = peopleRepository.findByLastname("Matthews", NamesOnly.class);
      }
      
  • JPA와 Projection

    • Spring Data JPA는 주로 튜플쿼리(필요한 필드만 조회)를 통한 interface-based Projections 지원 1. JPA Derived Queries
      • 메서드 이름으로 쿼리 만드는 방식
      • 반환 타입 보고 interface/class based projection 지원
      • 클래스 기반 프로젝션은 JPA의 생성자 표현식 따라감
        • interface-based: Tuple + 프록시 방식
        • class-based: SELECT new com.example.NamesOnlyDto(p.firstname, p.lastname) FROM Person p ... 2. 문자열 기반 (@Query)
      • JPQL과 class-based projection 쓸 때는 new 생성자로 매핑
      • 다만, 엔티티 전체를 조회하는 것 처럼 작성해도 스프링이 실제 DTO 타입보고 자체적으로 변환해줄 수도 있음.
        // 엔티티 전체를 조회하는 것처럼 작성
        // 스프링이 반환 타입 UserDto를 보고 아래와 같이 쿼리를 바꿔치기
        // SELECT new com.example.UserDto(u.firstname, u.lastname) FROM USER u WHERE u.lastname = :lastname
        @Query("SELECT u FROM USER u WHERE u.lastname = :lastname")
        List<UserDto> findByLastname(String lastname);
        
    1. Native Query
      • @SqlResultSetMapping를 사용할수도 있음. SQL 결과셋을 여기에 매핑해줘 표현