본문 바로가기

CS 잡지식

Projection 최적화 기능(feat.Spring Data JPA)

// 전체 엔티티를 조회하는 것이 아니라, select의 projection의 대상을 지정해서 들고 오고 싶을 때 Projections 기능 사용
public interface UsernameOnly {
    // 여기서는 Member 엔티티의 username, 딱 1개만을 지정해서 들고 오는 시나리오!

    
    String getUsername(); //이와 같은 형식(getter+필드명)으로 선언을 해주면 , Spring Data JPA가
    // "아!! USERNAME만 SELECT하면 되는구나"라고 판단하여, 해당 쿼리 구현체를 자동으로 만들어서,
    // UsernameOnly Proxy 객체에 username이 담긴 엔티티를 반환을 한다.

}

 

반환형(즉, UsernameOnly)에 정의된 필드값들이 Projection의 대상이 된다고 보면 된다. 

public interface MemberReposiotry extends JpaRepository<Member,Long>, MemberRepositoryCustom {

// Projectino 기능 사용
       List<UsernameOnly> findProjectionsByUsername(@Param("username") String username);

}

 

@Test@Transactional
public void projections(){

    Team teamA = new Team("teamA");
    entityManager.persist(teamA);

    Member m1 = new Member("m1", 0, teamA);
    Member m2 = new Member("m2", 0, teamA);
    entityManager.persist(m1);
    entityManager.persist(m2);

    entityManager.flush();
    entityManager.clear();

    // Member 엔티티 전체가 아닌 그 중의 username만 select의 projection으로 해서 들고 오고 싶다~!
    List<UsernameOnly> result = repository.findProjectionsByUsername("m1");
    for (UsernameOnly usernameOnly : result) {
        System.out.println("usernameOnly = " + usernameOnly);
    }

 

위와 같은 Projection을 Closed Projection이라고 부르고, Open Projcetion이라는 기능을 Spring Data JPA가 제공을 한다.

public interface UsernameOnly {
    // 여기서는 Member 엔티티의 username, 딱 1개만을 지정해서 들고 오는 시나리오!

    @Value("#{target.username + ' ' + target.age}") // USERNAME과 더불어 AGE도 가져 와서 담아 준다.
    String getUsername(); 
   
}

 

 

 

@Test@Transactional
public void projections(){

    Team teamA = new Team("teamA");
    entityManager.persist(teamA);

    Member m1 = new Member("m1", 0, teamA);
    Member m2 = new Member("m2", 0, teamA);
    entityManager.persist(m1);
    entityManager.persist(m2);

    entityManager.flush();
    entityManager.clear();


    List<UsernameOnly> result = repository.findProjectionsByUsername("m1");
    for (UsernameOnly usernameOnly : result) {
        System.out.println("usernameOnly = " + usernameOnly);
    }

그러나 Open Projection은 쿼리문이 다르다.

엔티티의 모든 필드를 projection해서 들고 온다. 

즉, 일단 모든 필드를 들고 온 후에, 후처리로 username과 age를 Proxy 객체에 담아서 반환을 한다.

 

위에서는 UsernameOnly라는 [인터페이스]를 사용하였기 때문에, 결과가 반환이 될 때, Spring이 

Proxy 객체의 형태로 결과값을 거기에 담아서 반환을 하였다. 

그러나, [인터페이스]가 아닌 클래스로 반환 타입을 정의를 해주면, Proxy 객체가 아닌 사용자가 정의한

엔티티(Dto)형태로 결과를 반환을 해 준다. 

 

public class UsernameOnlyDto {


    private final String username;

    // 이 생성자가 중요!
    public UsernameOnlyDto(String username){ // Spring Data JPA는 이 생성자의 매개변수 명(username)을
                                            //  Member 엔티티의 projection 대상이라고 보고, 결과를 UsernameOnlyDto에 담아서 반환을 한다.
                                           // 고로, 생성자의 매개변수명을 Member 엔티티의 필드값과 다르게 적으면 안됨.
        this.username = username;
    }

    public String getUsername(){
        return this.username;
    }



}

 

// Projectino 기능 사용
 List<UsernameOnly> findProjectionsByUsername(@Param("username") String username);

 // Proxy 객체가 아닌, UsernameOnlyDto 엔티티로 반환!!
List<UsernameOnlyDto> findProjectByUsername(@Param("username") String username);

 

@Test@Transactional
public void projections(){

    Team teamA = new Team("teamA");
    entityManager.persist(teamA);

    Member m1 = new Member("m1", 0, teamA);
    Member m2 = new Member("m2", 0, teamA);
    entityManager.persist(m1);
    entityManager.persist(m2);

    entityManager.flush();
    entityManager.clear();

    // Member 엔티티 전체가 아닌 그 중의 username만 select의 projection으로 해서 들고 오고 싶다~!
    List<UsernameOnlyDto> result = repository.findProjectByUsername("m1");
    for (UsernameOnlyDto usernameOnly : result) {
        System.out.println("usernameOnly = " + usernameOnly);
    }

 

 

제네릭 기법을 사용하여, [동적]으로 반환받고 싶은 클래스를 바꿀 수가 있다.

// Generic으로 해서, UsernameOnly으로 반환 받고 싶으면, UsernameOnly 타입을 매개변수로 넘기고,
// UsernameOnlyDto로 반환을 받고 싶으면, UsernameOnlyDto 타입을 매개변수로 넘기면 된다. 
// -> 동적으로 반환받고 싶은 엔티티 타입을 정의할 수가 있다. 
<T> List<T> findProByUsername(@Param("username") String username,Class<T> type);

 


// 사용 예시
List<UsernameOnlyDto> result = repository.findProjectByUsername("m1",UsernameOnlyDto.class);

 

[중첩 구조]에 있어서의 Projection 동작

// 중첩 구조(인터페이스 안에 또 다른 인터페이스, 즉 Proxy [객체] 안에, 또 다른 Proxy [객체]가 삽입된 반환형)
public interface NestedClosedProjections {

	// root : 맨 처음에 있는 것을 root라고 부르는데, root에 대해서는 최적화가 되지만, 그 이후부터는 안된다.
    String getUsername(); // username에 대해서는 최적화가 된다.( 쿼리문을 보면, username만이 select의 projection 대상이 된다.
    
    
    TeamInfo getTeam(); // 중첩 구조 안의 엔티티에 대해서는 최적화가 되지 않아서, 쿼리문을 살펴 보면, Team 엔티티의 [모든] 필드를 Projection해서
                        // 들고 온 뒤에, getName()을 통해서, team 이름을 엔티티로 삽입하여 반환을 한다.

   public interface TeamInfo{

        String getName(); // team 이름
    }


}

 

@Test@Transactional
public void projections(){

    Team teamA = new Team("teamA");
    entityManager.persist(teamA);

    Member m1 = new Member("m1", 0, teamA);
    Member m2 = new Member("m2", 0, teamA);
    entityManager.persist(m1);
    entityManager.persist(m2);

    entityManager.flush();
    entityManager.clear();

    List<NestedClosedProjections> result = repository.findProByUsername("m1", NestedClosedProjections.class);
    for (NestedClosedProjections nestedClosedProjections : result) {
        System.out.println("nestedClosedProjections = " + nestedClosedProjections);
    }

 

-> 중첩 구조에 있어서, username에 대해서는 최적화가 가능하지만(select의 projection 대상으로 username만 들어감)

그 안의 중첩 엔티티에 대해서는 최적화가 안된다(team 엔티티의 모든 필드를 select의 projection 대상으로 해서 DB로부

터 조회를 한 뒤, Spring Data JPA가 NestedClosedProjectinos 엔티티에 삽입을 하여서 반환을 한다.)

(Spring Data JPA는 root에 대해서만 Projection을 최적화하여 쿼리를 만들어 주지만, 그 이후부터는 최적화가 안된다.)

 

-> 위와 같이, Spring Data JPA가 제공하는 Projection 기능은 Projection 대상이 root 엔티티이면 최적화가 가능하지만

root 엔티티가 아닌 것에 대해서는 최적화를 해주지 않기 때문에, Projection 대상이 root 엔티티 하나만 있을 때에는 

유용하게 사용이 가능하지만, 그것을 넘어 가버리면 최적화가 안 돼서 사용하는 데에 있어서 한계가 존재.

쿼리가 단순할 땐, Projection 기능을 사용하고, 조금만 복잡해 지면 Querydsl로 쿼리문을 최적화하자!

(QueryDSL은 Projectino에 대해서 막강한 기능을 제공하기에, 위와 같은 root 엔티티 문제를 다 해결해준다)