본문 바로가기
Spring

[Spring Data JPA] Projections 간단 정리

by 개미가되고싶은사람 2025. 1. 20.

Projection은 데이터베이스에서 간단하게 필요한 필드만 선택하여 가져오는 기능입니다. 이를 통해 전체 엔티티를 조회하는 대신, 특정 필드만 포함된 객체를 생성할 수 있습니다. 즉, 성능을 향상시키고 데이터 전송을 최적화할 수 있습니다.

 

초기 설정(엔티티)

@Entity
public class Address {
 
    @Id
    private Long id;
 
    @OneToOne
    private Person person;
 
    private String state;
    private String city;
    private String street;
    private String zipCode;

    // getters and setters
}

@Entity
@Setter
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Person {
 
    @Id @GeneratedValue
    private Long id;
 
    private String firstName;
    private String lastName;
 
    @OneToOne(mappedBy = "person")
    private Address address;


    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

 

Closed Projections - 인터페이스 기반

Closed Projections는 필요한 필드만 포함된 인터페이스를 기반으로 프록시 객체를 생성하여, 엔티티에서 필요한 필드만 쿼리문으로 전송하는 방식입니다. 

public interface AddressProjections {
    String getZipCode();
}
public interface AddressRepository extends JpaRepository<Address, Long> {
    List<AddressProjections> getAddressByState(String state);
}
@SpringBootTest
@Transactional
@Rollback(false)
public class MemberRepositoryTest {

    @PersistenceContext
    EntityManager em;

    @Autowired
    AddressRepository addressRepository;

    @Test
    public void projections() {
        // given
        Person person = new Person("성", "이름");
        em.persist(person);

        Address address1 = new Address("서울", "금천구", "가산디지털2로", "12-34");
        Address address2 = new Address("용인", "기흥구", "경기도박물관", "75-14");
        em.persist(address1);
        em.persist(address2);

        em.flush();
        em.clear();
        
        // when
        List<AddressProjections> result = addressRepository.getAddressByState("서울");
        for (AddressProjections addressProjections : result) {
            System.out.println("addressProjections.getZipCode() = " + addressProjections.getZipCode());
        }

        //then
        assertThat(result.get(0).getZipCode()).isEqualTo("12-34");
    }
}

 

Open Projections - 인터페이스 기반

Open Projections도 인터페이스 기반으로 동작하며, 스프링의 SpEL 문법을 사용하여 동적으로 필드를 선택할 수 있습니다. Open Projections은 데이터베이스에서 엔티티의 모든 필드를 조회한 후, 필요한 필드를 자유롭게 변환해 반환하는 방식으로 작동합니다. 단 SELECT 절 최적화가 이루어지지 않고 반환 값을 반환하기 때문에 조금 아쉬운 부분이 있습니다.

public interface AddressProjections {
    @Value("#{target.state + ' ' + target.city + ' ' + target.street}")
    String getZipCode();
}

public interface AddressRepository extends JpaRepository<Address, Long> {
    List<AddressProjections> findProjectionsByZipCode(@Param("zipCode") String zipCode);

}
@SpringBootTest
@Transactional
@Rollback(false)
public class MemberRepositoryTest {

    @PersistenceContext
    EntityManager em;

    @Autowired
    AddressRepository addressRepository;

    @Test
    public void openProjections() {
        // given
        Person person = new Person("성", "이름");
        em.persist(person);

        Address address1 = new Address("서울", "금천구", "가산디지털2로", "12-34");
        Address address2 = new Address("용인", "기흥구", "경기도박물관", "75-14");
        em.persist(address1);
        em.persist(address2);

        em.flush();
        em.clear();
        // when
        List<AddressProjections> result = addressRepository.findProjectionsByZipCode("75-14");
        for (AddressProjections addressProjections : result) {
            System.out.println("addressProjections = " + addressProjections.getZipCode());
        }

        //then
		assertThat(result.get(0).getZipCode()).isEqualTo("용인 기흥구 경기도박물관");
    }
}

 

 

Class-Based Projections - 클래스 기반

Class-Based Projections는 특정 클래스 타입(DTO)을 기반으로 쿼리를 매핑하는 방식입니다. 이 방식에서는 특정 클래스를 지정하기 때문에 프록시 객체를 사용하지 않습니다. 

@Getter
public class PersonDto {
    private String firstName;
    private String lastName;

    public PersonDto(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

public interface PersonRepository extends JpaRepository<Person, Long> {
    List<PersonDto> findByFirstName(String firstName);
}
    @PersistenceContext
    EntityManager em;

    @Autowired
    PersonRepository personRepository;

	@Test
    public void classProjections() {
        // given
        Person person = new Person("성", "이름");
        em.persist(person);

        em.flush();
        em.clear();

        // when
        List<PersonDto> result = personRepository.findByFirstName("성");
        for (PersonDto personDto : result) {
            System.out.println("personDto = " + personDto.getClass());
            System.out.println("personDto.getFirstName() = " + personDto.getFirstName());
            System.out.println("personDto.getLastName() = " + personDto.getLastName());
        }
        //then
    }

 

 

Dynamic Projections - 동적

Dynamic Projections를 사용하면 동일한 메소드를 통해 동적으로 반환 타입을 변경할 수 있어 필요한 데이터만 선택적으로 가져올 수 있습니다. 예를 들어, 특정 상황에서는 zipcode와 state이 필요하고, 다른 상황에서는 state와 street이 필요할 때, 각각의 요구에 맞춰 DTO를 정의하고 사용할 수 있습니다.

public interface AddressRepository extends JpaRepository<Address, Long> {

    <T> List<T> findProjectionsByZipCode(@Param("zipCode") String zipCode, Class<T> type);

}
    @Test
    public void dynamicProjections() {
        // given
        Person person = new Person("성", "이름");
        em.persist(person);

        Address address1 = new Address("서울", "금천구", "가산디지털2로", "12-34");
        Address address2 = new Address("용인", "기흥구", "경기도박물관", "75-14");
        em.persist(address1);
        em.persist(address2);

        em.flush();
        em.clear();

        // when
        List<Address> result1 = addressRepository.findProjectionsByZipCode("75-14", Address.class);
        List<AddressDto> result2 = addressRepository.findProjectionsByZipCode("75-14", AddressDto.class);
        List<AddressDto2> result3 = addressRepository.findProjectionsByZipCode("75-14", AddressDto2.class);


    }

 

 

Nested Projection - 중첩 구조

중첩 프로젝션은 중첩된 DTO 클래스를 사용하면 연관된 엔터티를 서로 매핑할 수 있습니다. 프로젝션 대상이 루트 엔티티인 경우 SELECT 절을 최적화할 수 있으며, 프로젝션 대상이 루트가 아닌 경우 LEFT OUTER JOIN을 처리하여 모든 필드를 SELECT한 후 엔티티로 조회하고 계산을 수행합니다.

public interface AddressDto {
    String getZipCode();
    personInfo getPerson();

    interface personInfo {
        String getFirstName();
    }
}
    public void nestedProjection() {
        // given
        Person person = new Person("성", "이름");
        em.persist(person);

        Address address1 = new Address("서울", "금천구", "가산디지털2로", "12-34");
        Address address2 = new Address("용인", "기흥구", "경기도박물관", "75-14");
        em.persist(address1);
        em.persist(address2);

        em.flush();
        em.clear();
        // when
        List<AddressDto> result = addressRepository.findProjectionsByZipCode("75-14", AddressDto.class);
        for (AddressDto addressDto : result) {
            System.out.println("addressDto = " + addressDto);
        }
    }

 

 

결론

Projections은 간단한 쿼리문을 사용할 때 유용하지만 동적 쿼리나 복잡한 쿼리를 사용할 때 불편한 점이 있습니다. 특히 프로젝션 대상이 루트 엔티티가 아니라면 select절 최적화가 안되는 큰 문제를 가지고 있습니다. 이런 문제점을 네이티브 쿼리나 QueryDSL를 사용해 해결할 수 있습니다.

 

 

참고자료

https://docs.spring.io/spring-data/jpa/reference/repositories/projections.html

 

Projections :: Spring Data JPA

Spring Data query methods usually return one or multiple instances of the aggregate root managed by the repository. However, it might sometimes be desirable to create projections based on certain attributes of those types. Spring Data allows modeling dedic

docs.spring.io

https://www.baeldung.com/spring-data-jpa-projections

https://ndarkness.tistory.com/20

 

[JPA] Projection 의 다양한 방법들

Projection는 JPA로 조회시 원하는 컬럼을 가져오는 방식으로 Spring Data JPA 에서 사용하는 방식과 QueryDSL 에서 사용하는 방식으로 나눠서 볼 수 있습니다. Member.java @Data @Entity @Table(name = "member") public cl

ndarkness.tistory.com