Spring Data JPA 게시글은 대부분 인프런의 김영한님의 강의인 '실전! 스프링 데이터 JPA' 기반으로 내용을 정리했습니다.
Projections
이 기능은 약간의 도움이 될 때가 있어 앞 부분보다는 잘 듣는 게 좋다.
엔티티 대신에 DTO를 편리하게 조회할 때 사용한다. 예를 들면 Member 엔티티가 아니라 Member의 이름만 조회하고 싶을 때 유용하다.
인터페이스 기반의 Closed Projections
- UsernameOnly.interface
public interface UsernameOnly {
String getUsername();
}
- MemberRespository.interface
List<UsernameOnly> findProjectionsByUsername(@Param("username") String username);
특이한 점은 List 제네릭에 Member가 아닌 UsernameOnly 인터페이스가 들어간다. 이렇게 세팅해주면 사용준비 끝!
- MemberRepositoryTest.class
@Test
public void projections() {
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();
List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1");
for (UsernameOnly usernameOnly : result) {
System.out.println("usernameOnly = " + usernameOnly);
}
}
select
member0_.username as col_0_0_
from
member member0_
where
member0_.username=?
usernameOnly = org.springframework.data.jpa.repository.query
.AbstractJpaQuery$TupleConverter$TupleBackedMap@13cc3984
반환 된 result를 for문 돌리면서 usernameOnly를 찍어보면 이상한 것이 출력된다. 이렇게 인터페이스로 반환 타입을 지정하면 스프링 데이터 JPA에서 프록시 같은 기술들을 가지고 가짜 객체를 만든다. 즉 개발자가 인터페이스만 정의하면 구현체는 스프링 데이터 JPA가 알아서 만든다.
인터페이스 기반의 Open Projections
- UsernameOnly.interface
@Value("#{target.username + ' ' + target.age}")
String getUsername();
위와 같이 사용하면 Open Projections이다. Member(target)의 username과 age 데이터를 가져와서 Value에 있는 문자열 형식(SpEL)처럼 만든 후 리턴해준다.
select
member0_.member_id as member_i1_1_,
member0_.create_by as create_b2_1_,
member0_.create_date as create_d3_1_,
member0_.last_modified_by as last_mod4_1_,
member0_.last_modified_date as last_mod5_1_,
member0_.age as age6_1_,
member0_.team_id as team_id8_1_,
member0_.username as username7_1_
from
member member0_
where
member0_.username=?
usernameOnly = m1 0
age 데이터까지 가져와야하기 때문에 Member의 모든 데이터를 가져온다. 엔티티를 일단 가져오고 지정한 데이터를 가져와서 SpEL 형식처럼 뿌려주는 것이다. 그래서 JPQL SELECT 최적화가 되지 않는다.
클래스 기반의 Projections
- UsernameOnlyDto.class
public class UsernameOnlyDto {
private final String username;
public UsernameOnlyDto(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}
클래스 기반에서 중요한 건 생성자이다. 생성자의 파라미터 명으로 Projections이 동작한다. 조회하려는 엔티티의 필드 이름과 다르면 동작하지 않으므로 주의해야 한다.
- MemberRepository.interface
List<UsernameOnlyDto> findClassProjectionsByUsername(@Param("username") String username);
- MemberRepositoryTest.class
@Test
public void ClassProjections() {
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();
List<UsernameOnlyDto> result = memberRepository.findClassProjectionsByUsername("m1");
for (UsernameOnlyDto usernameOnly : result) {
System.out.println("usernameOnly = " + usernameOnly.getUsername());
}
}
select
member0_.username as col_0_0_
from
member member0_
where
member0_.username=?
usernameOnly = m1
동적 Projections
- MemberRepository.interface
<T> List<T> findGenericProjectionsByUsername(@Param("username") String username, Class <T> type);
제네릭을 사용하면 동적으로 Projections의 데이터를 변경할 수 있다.
- MemberRepositoryTest.class
List<UsernameOnlyDto> result =
memberRepository.findGenericProjectionsByUsername("m1", UsernameOnlyDto.class);
클래스 타입을 매개변수로 넘겨주면 알아서 딱 잘 맞춰서 동작한다.
중첩 구조 처리
- NestedClosedProjections.interface
public interface NestedClosedProjections {
String getUsername();
TeamInfo getTeam();
interface TeamInfo {
String getName();
}
}
인터페이스 안에 TeamInfo란 인터페이스를 하나 더 만들고 Team에서 가져올 필드까지 생성해주면 Member와 Team 데이터를 가져올 수 있다.
- MemberRepositoryTest.class
List<NestedClosedProjections> result =
memberRepository.findGenericProjectionsByUsername("m1", NestedClosedProjections.class);
for (NestedClosedProjections nestedClosedProjections : result) {
System.out.println("nestedClosedProjections = " + nestedClosedProjections);
}
select
member0_.username as col_0_0_,
team1_.team_id as col_1_0_,
team1_.team_id as team_id1_2_,
team1_.create_date as create_d2_2_,
team1_.update_date as update_d3_2_,
team1_.name as name4_2_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.username=?
nestedClosedProjections = org.springframework.data.jpa.repository.query
.AbstractJpaQuery$TupleConverter$TupleBackedMap@3733b1f4
쿼리를 잘 보면 Member의 데이터는 내가 원하는대로 username만 딱 가져왔지만 Team의 경우는 Name이 아닌 다른 데이터들도 모두 다 가져온 걸 확인할 수 있다. 중첩 구조는 제일 첫 번째 메서드(root)에 대해서만 쿼리 최적화가 되고 두 번째 메서드(getTeam)부터는 쿼리 최적화가 동작하지 않는다.
주의
- 프로젝션 대상이 root 엔티티면 JPQL SELECT절 최적화가 가능하다.
- 프로젝션 대상이 root가 아니면
- LEFT OUTER JOIN 발생
- 모든 필드를 SELECT해서 엔티티로 조회한 다음에 후처리
정리
- 프로젝션 대상이 root 엔티티면 유용하게 사용할 수 있다.
- 프로젝션 대상이 root 엔티티를 넘어가면 JPQL SELECT 쿼리 최적화가 안 된다.
- 실무의 복잡한 쿼리를 해결하기에는 한계가 있다.
- 실무에서는 단순할 때만 사용하고, 조금 복잡해지면 QueryDSL을 사용하자.