*인프런 '실전! 스프링부트와 JPA 활용2' 강의 내용에서 배운 내용을 정리한 것입니다*

 

-대부분의 페이징+컬렉션 엔티티 조회문제는 다음과 같은 방법으로 해결할 수 있다 

1. ToOne관계는 모두 페치조인한다. ToOne관계는 row수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다

 

2. 컬렉션은 지연로딩으로 조회한다 

 

3. 지연로딩 성능 최적화를 위해 hibernate.default_bath_fetch_size, @BatchSize를 적용한다.

-hibernate.defqult_bath_fetch_size: 글로벌 설정(applications.yml에서 설정)

-@BatchSize: 개별 최적화 (컬렉션은 컬렉션 필드에, 엔티티는 엔티티 클래스에 적용한다)

이 옵션을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 sizea만큼 IN 쿼리로 조회한다 

 

spring: jpa:
        properties:
          hibernate:
            default_batch_fetch_size: 1000

-장점

쿼리 호출 수가 1+N에서 1+1로 최적화 된다

조인보다 DB데이터 전송량이 최적화된다

DB데이터 전송량이 감소한다

컬렉션 페치 조인은 페이징이 불가능하지만, 이 방법은 페이징이 가능하다

-결론

ToOne관계는 페치 조인을 해도 페이징에 영향을 주지 않기 때문에 그냥 페치조인을 사용해서 쿼리 수를 줄이고, 나머지는 hibernate.default_batch_fetch_size로 최적화 하자

*인프런 '실전! 스프링부트와 JPA 활용2' 강의 내용에서 배운 내용을 정리한 것입니다*

 

이전 내용과 같은 방식으로 엔티티를 직접 노출하지 않고 DTO로 반환하고, 지연로딩을 사용하며, 1+N문제를 해결하기 위해 페치조인을 사용해서 컬렉션을 조회한다 

/OrderApiController 

@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
    List<Order> orders = orderRepository.findAllWithItem();
    List<OrderDto> result = orders.stream()
            .map(o -> new OrderDto(o))
            .collect(toList());
    return result;
}

/OrderRepository

public List<Order> findAllWithItem() {
        return em.createQuery(
"select distinct o from Order o" +
        " join fetch o.member m" +
        " join fetch o.delivery d" +
        " join fetch o.orderItems oi" +
        " join fetch oi.item i", Order.class)
.getResultList();
}

컬렉션을 조회하는 경우는 일대다(@OneToMany),다대다(ManyToMany)이므로 조회하게되면 데이터베이스 row가 증가한다. 하나의 order에 여러개의 orderItems가 있는 경우, orderItems에 맞춰서 여러개의 order row가 생길 수 있다.     

이때, distinct를 사용하면 중복을 걸러서 조회할 수 있다

 

-JPQL에서 distinct 기능 2가지 

1. 디비에 distinct 키워드 날려주고 

2. 엔티티가 중복인 경우에 중복을 걸러서 컬렉션에 담아준다 

 

하지만 distinct를 사용하면 페이징이 불가능하다 

 

참고: 컬렉션 페치 조인을 사용하면 페이징이 불가능하다.

두개 이상의 컬렉션에 페치 조인을 사용하면 데이터가 부정합하게 조회될 수 있어 안된다 

(OrderItem과 Item은 다대일 관계이므로, orderitem을 조회할 때 Item도 같이 조회 된다)

 

 

*인프런 '실전! 스프링부트와 JPA 활용2' 강의 내용에서 배운 내용을 정리한 것입니다*

 

참고: dto는 엔티티를 참조해도 괜찮다

 

1. 엔티티를 DTO로 변환하는 방법을 선택한다

2. 필요하면 페치조인으로 성능을 최적화한다 (대부분의 성능 이슈가 해결된다)

 

예시)

/OrderRepository

public List<Order> findAllWithMemberDelivery() {
      return em.createQuery(
 }
"select o from Order o" +
        " join fetch o.member m" +
        " join fetch o.delivery d", Order.class)
.getResultList();

 

==> 내가 원하는 것만 select 하고, 외부의 모습은 건드리지 않은 상태로 내부에서 원하는 것으로만 성능을 튜닝할 수 있다.

엔티티를 조회했기 때문에 비즈니스 로직을 써서 데이터 변경을 할 수 있다.

재사용성이 있다

 

3. 그래도 안되면 JPA에서 직접 DTO를 조회하는 방법을 사용한다 

 

예시)

/OrderSimpleQueryRepository

select 프로젝션에 new와 파일 경로를 설정한 다음 필요한 정보들을 다 입력한다

@Repository
@RequiredArgsConstructor
public class OrderSimpleQueryRepository {
  
      private final EntityManager em;
      
      public List<OrderSimpleQueryDto> findOrderDtos() {
          return em.createQuery(
                  "select new
  jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryDto(o.id, m.name,
  o.orderDate, o.status, d.address)" +
  		" from Order o" +
        " join o.member m" +
        " join o.delivery d", OrderSimpleQueryDto.class)
		.getResultList();

   } 
 }

===> 한번에 내가 원하는 데이터를 JPQL을 짜서 가져온다. 해당 DTO에만 사용 가능하기 때문에 딱딱하다

데이터 변경 불가능하고, API 스펙이 리포지토리에 들어가 있기 때문에 논리적 계층이 깨진다(repository는 엔티티 조회 용도로 사용해야한다)

 

4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다 

 

1. 엔티티를 API에 노출하는 방법을 사용한다면 안되는 이유

-엔티티에 프레젠테이션 계층을 위한 로직이 추가된다

-엔티티에 API 검증을 위한 로직(@NotEmpty)이 들어간다

이는 엔티티에 validation을 위한 로직을 포함하는 것이다 

-실무에서는 회원 엔티티를 위해 다양한 API가 만들어지는데, 한 엔티티에 각 API의 모든 요청 요구사항을 담기는 어렵다

-엔티티가 변하면 API 스펙이 변한다(가장 큰 문제점)

 

-엔티티를 파라미터로 받아서 사용하는 saveMemberV1 메소드

@PostMapping("/api/v1/members")
    public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member)
{
        Long id = memberService.join(member);
        return new CreateMemberResponse(id);
   
}
 
 
@Data
    static class CreateMemberResponse {
    private Long id;
        
   public CreateMemberResponse(Long id) {   
		this.id = id;
              
     } 
}

---> 엔티티를 파라미터로 받지 말자. 엔티티를 노출해서도 안된다 

2. 위 문제를 해결하는 방법

API 요청 스펙에 맞추어 별도의 DTO를 파라미터로 받는다 

CreateMemberRequest를 Member 엔티티 대신에 RequestBody와 매핑한다

 

이렇게 하면, 

엔티티와 프레젠테이션 계층을 위한 로직을 분리할 수 있다

엔티티를 변경해도 API 스펙이 변하지 않는다 

엔티티와 API 스펙을 명확하게 분리할 수 있다 

@PostMapping("/api/v2/members")
      public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request) {
  
          Member member = new Member();
          member.setName(request.getName());
          Long id = memberService.join(member);
          return new CreateMemberResponse(id);
          
}

@Data
    static class CreateMemberRequest {
        private String name;
    }
    
@Data
static class CreateMemberResponse { 
	private Long id;
    public CreateMemberResponse(Long id) {
    	this.id = id;                 		 
}

 

 

 

+ Recent posts