3 분 소요

나머지 기능

거의 안쓰는 기능 (알고만 있자.)

엔티티 대신에 DTO를 편리하게 조회할 때 사용 전체 엔티티가 아니라 만약 회원 이름만 딱 조회하고 싶으면?

먼저 repository패키지에 UsernameOnly 인터페이스를 만들었다.

1
2
3
public interface UsernameOnly {  
    String getUsername();  
}

내가 필요한 username을 getter 만드는 느낌으로 이름을 getUsername()으로 한다. 만약에 Id 가 필요했다면 Long getId(); 이런 걸 추가 하면 되지 않을까?

그 후 MemberRepository에서 다음과 같이 사용 했다.

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

이러고 테스트를 진행해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test  
public void projections() throws Exception {  
    //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");  
    //then  
    assertThat(result.get(0).getUsername()).isEqualTo("m1");  
}

정말 재밌게도 엔티티를 조회할 땐 엔티티에 속성들이 모두 select 절에 들어갔지만, 지금은 UsernameOnly인터페이스에 정의된 getUsername 만 select 절에 가져오는 걸 볼 수 있다.

또 스프링이 알아서 인터페이스로 만든 UsernameOnly을 알아서 객체로 구현해서 리턴 해주는 것도 알 수 있다.

OpenProjection

1
2
3
4
public interface UsernameOnly {  
    @Value("#{target.username + ' ' + target.age}")  
    String getUsername();  
}

이런 것도 가능하다. userName에 원하는 데이터를 만들 수도 있다.

하지만 왜 OpenProjection이냐면, 이게 실행 될 때 Entity를 조회하듯이 모든 걸 다 조회한 후에
@Value("#{target.username + ' ' + target.age}") 여기서 정의한 대로 만들어 주기 때문이다.

이렇게 바꿔 놓고 테스트를 또 진행해보면

이름 + 나이 가 이름으로 출력 된걸 볼 수 있다.

클래스 형식 Projections

다음과 같은 형태도 가능하다.

UsernameOnlyDto 클래스를 만들어 보자.

1
2
3
4
5
6
7
8
9
10
11
public class UsernameOnlyDto {  
    private final String username;  
  
    public UsernameOnlyDto(String username) {  
        this.username = username;  
    }  
  
    public String getUsername() {  
        return username;  
    }  
}

다음과 같은 클래스를 만들었고,이때 중요한 건 생성자에 들어가는 파라미터 이름이다. 이게 우리가 조회하려는 엔티티의 이름과 일치해야 조회가 잘 이루어 진다.

그 다음 getter를 세팅해 주면 된다.

이제 MemberRepository에서 조회 메서드를 살짝 수정해 보자

1
<T> List<T> findProjectionsByUsername(@Param("username") String username, Class<T> type);

다음과 같이 아무 타입을 받아서 넘기겠다는 의미로 작성했다.

그럼 테스트에선

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test  
public void projections() throws Exception {  
    //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<UsernameOnlyDto> result = memberRepository.findProjectionsByUsername("m1", UsernameOnlyDto.class);  
    //then  
    assertThat(result.get(0).getUsername()).isEqualTo("m1");  
}

when절에서 호출하는 방식으로 호출 하면 원하는 클래스로 받을 수 있게 되는 것이다.

쿼리도 내가 원하는 방식으로 조회가 됐다.

혹시 여기서 JPA 3.0 이상이라 오류가 난다면 인텔리제이에서 gradle 설정에서 빌드를 gradle로 바꾸면 될 거다.

아무튼 이렇게 해놓으면, 동적으로 Projection을 할 수 있게 된다.

중첩 구조 Projections

멤버랑 팀을 함께 Projections를 해보자.

1
2
3
4
5
6
7
public interface NestedClosedProjections {  
    String getUsername();  
    TeamInfo getTeam();  
    interface TeamInfo {  
        String getName();  
    }  
}

다음과 같이 인터페이스를 만들었고 그 안에 Team에 대한 인터페이스를 만들었다.

일단 동적으로 MemberRepositoryfindProjectionsByUsername() 메서드를 만들어 놨기 때문에 테스트 코드만 살짝 수정해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test  
public void projections() throws Exception {  
    //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);  
    //then  
    assertThat(result.get(0).getUsername()).isEqualTo("m1");  
    assertThat(result.get(0).getTeam().getName()).isEqualTo("teamA");  
}

자 돌려보자.

통과는 잘 됐는데, Team에 대한 컬럼들은 최적화가 안된다.

주의

  • 프로젝션 대상이 ROOT 엔티티면, JPQL SELECT절 최적화 가능
  • 프로젝션 대상이 ROOT 가 아니라면
    • LEFT OUTER JOIN 처리
    • 모든 필드를 SELECT해서 엔티티로 조회한 다음 계산

정리

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

태그: ,

카테고리:

업데이트:

댓글남기기