- Published on
Spring JPA의 사실과 오해 (2)
- Authors
- Name
- Chan Sol OH
목차
- JPA Repository 메서드로 JOIN 쿼리 보내기
- Page vs Slice
- JPA Repository 메서드로 DTO Projection 하기
- Class 기반 Projection
- Interface 기반 Projection
- Dynamic Projection
이번 포스팅은 Hibernate의 N+1 Select 문제에서 추가적으로 NHN Forward 2019에 발표된 Spring Data JPA의 사실과 오해
내용을 참고하고 실습한다.
이전 OneToMany 관계의 insert 쿼리 문제와 fetch join에 관련된 내용은 Spring JPA의 사실과 오해 (1)을 참고할 수 있다.
JPA Repository 메서드로 JOIN 쿼리 보내기
JPA는 데이터베이스 테이블 간의 관계를 Entity 클래스의 속성으로 모델링한다. JPA Repository 메서드는 _
를 통해 내부 속성값을 접근할 수 있다.
ProductContent
Entity 객체의 product 속성 값에 접근해서 N대1 관계인 ProductContent
와 Product
두 테이블을 Left Join해서 쿼리를 요청할 수 있다.
public interface ProductContentRepository extends JpaRepository<ProductContent, Long> {
List<ProductContent> findByProduct_ProductId(Long id);
List<ProductContent> findByProduct_ProductName(String name);
}
ProductContentRepository
에서 findByProduct_ProductId
는 프록시 객체인 id만 참조하기 때문에 Left Join이 발생하지 않는다. 하지만, findByProduct_ProductId
로 얻은 ProductContent 객체 내부의 Product 속성 중 id를 제외한 속성을 참조하면 N+1문제가 발생할 것이다.
findByProduct_ProductName
을 사용하면 ProductContent Entity의 Product 속성 중 내부 속성인 name을 검색하기 때문에 SQL 쿼리를 작성할 때 Left Join이 적용되어서 DB에 요청된다. 실제 요청에 따른 쿼리는 아래와 같다.
select p1_0.content_id,p1_0.content_img_url,p1_0.product_id
from product_content p1_0
left join product p2_0
on p2_0.product_id=p1_0.product_id
where p2_0.product_name=?
Page vs Slice
JPA Repository 메서드 중 Page
와 Slice
자료형을 출력하는 메서드는 쿼리의 결과 중 특정 개수의 row data에 대해서만 응답하도록한다. 하지만, 아래 그림과 같이 Page
는 Slice
를 상속 받으며 전체 페이지와 전체 Element 개수를 검색하는 것을 확인할 수 있다.
Page는 조건에 따른 전체 요소 개수를 세기위해서 count
쿼리를 한번 더 요청하는 문제가 있다. 그렇기 때문에 데이터가 급변하지 않는다면, 초기에 한번 count
쿼리를 날려서 전체 개수를 얻고, Slice
를 사용하여 page에 따른 요소를 얻는 것이 단순히 Page
를 사용하는 것보다 적절할 것이다.
JPA Repository 메서드로 DTO Projection 하기
JPA Repository 메서드로 Entity를 DTO로 Projection하는 방법은 아래와 같이 3가지 방법이 있다.
- Class 기반 Projection
- Interface 기반 Projection
- Dynamic Projuection
Class 기반 Projection
아래와 같이 DTO에 final
로 선언된 필드를 JPA Repository의 출력 자료형으로 사용하면 final
필드와 동일한 컬럼만 쿼리의 결과로 나오게된다.
@Value
public class MemberDto{
private final String name;
private final LocalDate createDate;
}
public interface MemberRepository extends JpaRepository<Member, Long>{
Collection<MemberDto> findByName(String name);
}
// name과 create_date 컬럼만 쿼리된다. -> SQL 자체적으로 DTO projection 가능!
Interface 기반 Projection
아래와 같이 Member
Entity가 있을 때, Entity class 필드의 getter를 정의한 interface MemberNameOnly
를 작성할 수 있다.
@Entity
@Table(name="Members")
public class Member{
private String name;
}
public interface MemberNameOnly {
String getName();
}
위처럼 Entity와 Interface를 정의하고, JPA Repository의 출력 자료형에 Interface를 넣으면
public interface MemberRepository extends JpaRepository<Member, Long>{
Collection<MemberNameOnly> findByCreateDateAfter(LocalDateTime createDate);
}
// select name from Members
// where create_dt > {createDate}
Spring Data JPA가 실제 구현 객체는 프록시로 만들어서 출력하게된다. 그리고 Interface 기반 Projection은 중첩 구조를 지원한다.
@Entity
public class Member{
private String name;
@OneToMany
private List<MemberDetail> details;
}
@Entity
public class MemberDetail{
@EmbeddedId
private Pk pk;
private String description;
@EmbeddedId public static class Pk{
private String type;
}
}
예를 들어 Member
에 List<MemberDetail>
이라는 필드가 존재할 때 아래와 같이 Interface를 작성할 수 있다.
public interface MemverDto{
String getName();
List<MemberDetailDto> getDetails();
interface MemberDetail{
@Value("#{target.pk.type}")
String getType();
String getDescription();
}
}
@Value
를 통해서 타켓을 설정하고, 인터페이스의 중첩 구조를 통해 Spring Data JPA의 출력 DTO를 정의할 수 있다.
Dynamic Projection
Dynamic Projection은 기본 Projection 방법들과 다르게 제네릭을 이용해서 출력 자료형이 정해지지 않고 JPA Repository method로 쿼리의 조건만 정의해 놓아서 유연한 사용을 할 수 있다.
public interface MemberRepository extends JpaRepository<Member, Long>{
<T> Collection <T> findByCreateDateAfter(LocalDate createDate, Class<T> type);
}
위 메서드는 아래와 같이 사용할 수 있다.
Collection<MemberNameOnly> nameOnlies = memberRepository.findByCreateDateAfter(LocalDateTime.now(), MemberNameOnly.class)
Collection<MemberDto> nameOnlies = memberRepository.findByCreateDateAfter(LocalDateTime.now(), MemberDto.class)