Spring/SpringDataJPA

Projections

느리지만 꾸준하게 2022. 4. 19. 17:18

DB에서 엔티티를 조회하고 싶을 때도 있지만 회원의 이름만 조회하고 싶을때가 있다.

JPA를 통해서 조회를 하게되면 엔티티를 통채로 조회하게 되면 쿼리의 데이터가 엄청 커지게 된다.

이름만 조회를 하고 싶을때 편하게 하는 방법을 Projections라고 한다.

 

쿼리에 SELECT절에 들어갈 데이터라고 보면 된다.

 

 

 

UsernameOnly Interface를 만들고

 

package study.datajpa.repository;

public interface UsernameOnly {
    String getUsername();
}

 

 

MemberRepository Interface에 아래구문을 추가해준다.

// 엔티티가 아니라 인터페이스 UsernameOnly에 구현체 프록시 객체가 담겨서 간다.
    List<UsernameOnly> findProjectionsByUsername(@Param("username") String username);

 

projections 테스트를 돌려보면 

@Test
public void projections() {
    // given
    Team teamA = new Team("teamA");
    em.persist(teamA);

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

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

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

    }

}

 

 

Debug에서 stepover을 한번 눌러 준 후에 console을 확인해보면 

 

usernameOnly = org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap@45e68fac

 

member의 username만 정확하게 찍어온것을 볼 수 있다.

username이 m1으로 정확하게 들어간 것을 볼 수 있다.

select
        member0_.username as col_0_0_ 
    from
        member member0_ 
    where
        member0_.username=?
        
select member0_.username as col_0_0_ from member member0_ where member0_.username='m1';

 

 

getUsername해서 찍어보면 아래와 같다.

즉 인터페이스 해서 찍어보면 구현클래스는 스프링 데이터 JPA가 프록시 같은 기술들을 가지고 username 속성만 찍으면 되는구나 해서 판단을 하고 구현체까지 데이터에 담아서 반환을 해준다.

// UsernameOnly Interface

package study.datajpa.repository;

public interface UsernameOnly {
    String getUsername();
}

 

 

위에서 했던 것은 Close Projections이였다.

 

Open Projections을 살펴보면

// UsernameOnly

package study.datajpa.repository;

import org.springframework.beans.factory.annotation.Value;

public interface UsernameOnly {


    @Value("#{target.username + ' ' + target.age}")
    String getUsername();
}

 

member 엔티티를 다 가지고 와서 처리를 하게된다.

select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.created_date as created_3_1_,
        member0_.tema_id as tema_id5_1_,
        member0_.username as username4_1_ 
    from
        member member0_ 
    where
        member0_.username=?

 

 

인터페이스 기반의 실제 SELECT Projections을 살펴보자

// UsernameOnlyDto

package study.datajpa.repository;

public class UsernameOnlyDto {

    private final String username;

    // constructor의 파라미터 이름으로 매칭을 시켜서 projections도 된다.
    public UsernameOnlyDto(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }
}

 

test절을 아래와 같이 UsernameOnlyDto로 바꿔주고

//when
List<UsernameOnlyDto> result = memberRepository.findProjectionsByUsername("m1");
for (UsernameOnlyDto usernameOnly : result) {
    System.out.println("usernameOnly = " + usernameOnly.getUsername());

}

MemberRepository Interface도 아래와 같이 바꿔준다.

List<UsernameOnlyDto> findProjectionsByUsername(@Param("username") String username);

m1이 출력되는 것을 확인할 수 있다.

usernameOnly = m1

 

debug를 돌리고 stepover를 눌러보면

//when
List<UsernameOnlyDto> result = memberRepository.findProjectionsByUsername("m1");
for (UsernameOnlyDto usernameOnly : result) {
    System.out.println("usernameOnly = " + usernameOnly.getUsername());

}

 

projections이 정확하게 실행이 되고

select
        member0_.username as col_0_0_ 
    from
        member member0_ 
    where
        member0_.username=?

 

result에 정확하게 usernameOnlyDto라고 되어있다.

 

 

 

 

동적 projections이라고 해서 Generic Type을 줄 수도 있다.

// MemberRepository Interface

<T> List<T> findProjectionsByUsername(@Param("username") String username, Class<T> type);
// MemberRepositoryTest에서 UsernameOnlyDto.class를 넣어준다.

//when
List<UsernameOnlyDto> result = memberRepository.findProjectionsByUsername("m1", UsernameOnlyDto.class);
for (UsernameOnlyDto usernameOnly : result) {
    System.out.println("usernameOnly = " + usernameOnly.getUsername());

}

 

같은 실행 쿼리를 확인할 수 있다.

select
        member0_.username as col_0_0_ 
    from
        member member0_ 
    where
        member0_.username=?
2022-04-19 16:59:02.641  INFO 72940 --- [           main] p6spy                                    : #1650355142641 | took 0ms | statement | connection 3| url jdbc:h2:tcp://localhost/~/datajpa
select member0_.username as col_0_0_ from member member0_ where member0_.username=?
select member0_.username as col_0_0_ from member member0_ where member0_.username='m1';
usernameOnly = m1

 

 

 

member와 연관된 team까지 가지고 오는 중첩쿼리를 살펴보자

아래와 같이 NestedClosedProjections Interface를 만들고 MemberRepositoryTest에 NestedClosedProjections로 변경을 해준다.

// NestedClosedProjections Interface


package study.datajpa.repository;

public interface NestedClosedProjections {

    String getUsername();
    TeamInfo getTeam();
    
    interface TeamInfo {
        String getName();
    }
}
// MemberRepositoryTest

//when
        List<NestedClosedProjections> result = memberRepository.findProjectionsByUsername("m1", NestedClosedProjections.class);
        for (NestedClosedProjections nestedClosedProjections : result) {
            System.out.println("nestedClosedProjections = " + nestedClosedProjections);
        }

        }

debug를 돌려보면

member는 하나만 가져오는데 team은 다 가지고 온다.

left outer join을 하게된다. 

 select
        member0_.username as col_0_0_,
        team1_.team_id as col_1_0_,
        team1_.team_id as team_id1_2_,
        team1_.created_date as created_2_2_,
        team1_.updated_date as updated_3_2_,
        team1_.name as name4_2_ 
    from
        member member0_ 
    left outer join
        team team1_ 
            on member0_.tema_id=team1_.team_id 
    where
        member0_.username=?

 

team은 엔티티로 불러오게 되서 team을 다가져오게 된다.

NestedClosedProjections Interface

package study.datajpa.repository;

public interface NestedClosedProjections {

    String getUsername();
    TeamInfo getTeam();

    
    // 두번째 부터 최적화가 안된다.
    interface TeamInfo {
        String getName();
    }
}

 

 

test를 정확하게 확인을 하기 위해서 아래와 같이 설정해주고

@Test
public void projections() {
    // given
    Team teamA = new Team("teamA");
    em.persist(teamA);

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

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

    //when
    List<NestedClosedProjections> result = memberRepository.findProjectionsByUsername("m1", NestedClosedProjections.class);
    for (NestedClosedProjections nestedClosedProjections : result) {
        String username = nestedClosedProjections.getUsername();
        System.out.println("username = " + username);
        String teamName = nestedClosedProjections.getTeam().getName();
        System.out.println("teamName = " + teamName);
    }

 

 

member 루트는 정확하게 가져오는데 연관된 team은 엔티티를 조회한 다음에 거기서 다시 계산하는 식으로 돌아가서 다 가져오게 된다.

 

username과 teamName은 데이터가 정확하게 원하는 대로 나오는 것을 확인할 수 있다.

select
        member0_.username as col_0_0_,
        team1_.team_id as col_1_0_,
        team1_.team_id as team_id1_2_,
        team1_.created_date as created_2_2_,
        team1_.updated_date as updated_3_2_,
        team1_.name as name4_2_ 
    from
        member member0_ 
    left outer join
        team team1_ 
            on member0_.tema_id=team1_.team_id 
    where
        member0_.username=?


username = m1
teamName = teamA

 

아래와 같이 정리된다.

  • 프로젝션 대상이 root 엔티티면, JPQL SELECT 절 최적화 가능하고
  • 프로젝션 대상이 ROOT가 아니면
  •  

 

  • LEFT OUTER JOIN 처리
  • 모든 필드를 SELECT해서 엔티티로 조회한 다음에 계산

 

정리

  • 프로젝션 대상이 root 엔티티면 유용함
  • 프로젝션 대상이 root 엔티티를 넘어가면 JPQL SELECT 최적화가 안됨.
  • 실무의 복잡한 쿼리를 해결하는데 한계가 있음.
  • 실무에서 단순할 때만 사용하고, 조금 복잡해지면 QueryDSL을 사용하자!

 

 

 

 

 

 

 

 

 

 

<출처 김영한: 실전! 스프링 데이터 JPA >

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard

 

실전! 스프링 데이터 JPA - 인프런 | 강의

스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다.

www.inflearn.com

 

'Spring > SpringDataJPA' 카테고리의 다른 글

네이티브 쿼리  (0) 2022.04.19
Query By Example  (0) 2022.04.19
Specifications(명세)  (0) 2022.04.19
새로운 엔티티를 구별하는 방법  (0) 2022.04.18
스프링 데이터 JPA 구현체 분석  (0) 2022.04.18