실전! 스트링 부트와 JPA 활용 2 - 간단한 주문 조회

이태휘·2022년 11월 1일
0
post-custom-banner

지연 로딩과 조회 성능 최적화

주문 + 배송정보 + 회원을 조회하는 API를 만들기
-> 지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해볼것

  • 매우 중요한 내용임!
    -> 지금부터 나오는것을 알아야 JPA를 실무에서 사용 가능

간단한 주문 조회 V1 : 엔티티 직접 노출

    private final OrderRepository orderRepository;

    //이건 그냥 반환하면 큰 문제가 생김 - 무한루프에 빠짐
    //오더에서 멤버갓다가 멤버에서 오더갓다가 왓다갓다해서 무한루프
    //따라서 이렇게 쓸려면 한쪽에 @JsonIgnore을 써줘야해
    @GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1(){
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        return all;
    }

-> order domain 에서 멤버가 지연로딩으로 되어있음 그래서 new해서 멤버객체가져오는게 아니라 멤버는 손을 안댐. 지연로딩이라
-> 따라서 hibernate는 proxy멤버를 생성해서 넣어놔 가짜멤버를 넣어놓고 멤버객체를 실제로 사용하려할때 디비에 그때 접근해서 가져옴
-> 근데 저 V1은 프록시로 자리만 대체해놨기 때문에 JSON이 값을 가지고 올 수 없어서 오류가 난거임.

하이버네이트 모듈 설치하기

-> 지연로딩 무시하라는 의미를 가지게됨

@SpringBootApplication
public class JpashopApplication {

	public static void main(String[] args) {
		SpringApplication.run(JpashopApplication.class, args);
	}

	@Bean
	Hibernate5Module hibernate5Module(){
		return new Hibernate5Module();
	}
}

-> 이러면 아까 proxy 관련 문제를 해결할 수 있는데 가볍게 듣자! 왜냐하면 v1은 안좋은 방법이기 때문
-> 엔티티를 그대로 노출해서 API 스펙 노출하기도하고 성능상으로도 안좋아서
-> force lazy loading을 해주기 싫으면 getmapping안에 iteration 사용해서 강제로 getname() 해주는 방법도 있음

즉, DTO로 변환해서 반환하는 습관을 들이자!

간단한 주문 조회 V2 : 엔티티를 DTO로 변환

  • API 스펙을 명확히 하자!
//v2
    @GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2(){
        return orderRepository.findAllByString(new OrderSearch()).stream()
                .map(SimpleOrderDto::new)
                .collect(Collectors.toList());
    }

    //배송관련 정보
    @Data
    static class SimpleOrderDto{
        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;

        //생성자로 필요한거 완성시키기
        public SimpleOrderDto(Order order){
            orderId = order.getId();
            name = order.getMember().getName();
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();
        }
    }

-> 주문 두개 잘 나오는게 보임!
-> API 스펙에 맞춰서 최적화돼서 잘 나옴
-> 이렇게하면 엔티티값 바뀌어도 돼고 엔티티 속성 이름 바껴도 바로 컴파일 오류나서 캐치를 할 수 있으니까 편함!

하지만 v1, v2 모두 lazy로딩으로 인해 쿼리가 너무 많이 호출된다는 단점이 있어. 즉, 테이블을 3개나 조회를 해야하기때문!
-> 오더, 멤버, 딜리버리 세개를 조회해야해

1) Order -> SQL 1번 실행하는데 결과 주문수 2개 나와서 루프를 두번 돌아 (주문 하나를 위한 DTO 생성될때마다)
-> 주문 한번당 2번 쿼리 돌려서 총 5번의 쿼리가 나가게돼

이걸 N+1 문제라해.
-> 첫번쨰 쿼리(1)을 날리고, 그거의 결과로 N번만큼(주문수) 쿼리 실행돼
-> 여기선 회원당 배송이 N 번이라 1+N+N해서 5번 쿼리터짐
-> 성능이 많이 안좋아져!

-> 즉, 같은 멤버에 대한 쿼리는 생략을 함. 영속성 컨텍스트에서 조회해서!
쨌든 성능이 안좋긴해.

  • 따라서 모든 연관관게를 lazy로 해놓고, 필요하면 fetch 조인을 하는것이 좋음!

간단한 주문 조회 V3 : 엔티티를 DTO로 변환 - 패치 조인 최적화

  • OrderRepository 에 만들어주기
	//오더를 조인하는데 멤버랑 딜리버리를 한번에 조인해서 다 가져오는 것
    //lazy 무시하고 다 값을 채워서가져옴
    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();
    }

-> 새로운 함수 만들어줘서 fetch join 함
-> 실무에서 정말 자주함. fetch join은 백프로 이해를 하고 사용을 하자!
-> 기본적인건 lazy로 깔고, 내가 필요한거만 fetch join으로 디비에서 가져오면 대부분의 성능 문제가 해결이 됨

  • v3 코드
//패치조인쓰는 버전
    //v2, v3는 결과적으론 똑같지만 날리는 쿼리의 개수가 다름
    @GetMapping("/api/v3/simple-orders")
    public List<SimpleOrderDto> ordersV3() {
        List<Order> orders = orderRepository.findAllWithMemberDelivery();
        List<SimpleOrderDto> result = orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());
        
        return result;
    }

같은 결과를 얻는데 v2는 5번의 쿼리를 날리는 반면, v3는 쿼리를 한번만 날림.
-> 페치 조인으로 order -> member , order -> delivery 는 이미 조회 된 상태 이므로 지연로딩X

-> 패치조인 적극적으로 활용하자!

간단한 주문 조회 V4: JPA에서 DTO로 바로 조회

-> 엔티티를 DTO로 중간에 변환하는 과정없이 바로 JPA 에서 데이터로 가져오는것

Repository 에 함수 만들어줄때 리포지토리랑 컨트롤러 의존관계 생기는거 주의하자! 지워줘야 안생김!! 생기면 큰일!!

  • OrderSimpleQueryDto 를 레포지토리에 생성
package jpabook.jpashop.repository;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.OrderStatus;
import lombok.Data;

import java.time.LocalDateTime;

@Data
public class OrderSimpleQueryDto {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

    //생성자로 필요한거 완성시키기
    public OrderSimpleQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address){
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }
}
  • OrderRepository 에 함수 추가
public List<OrderSimpleQueryDto> findOrderDtos() {
        return em.createQuery(
                "select new jpabook.jpashop.repository.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();
    }
  • v4 코드
@GetMapping("/api/v4/simple-orders")
    public List<OrderSimpleQueryDto> ordersV4() {
        return orderRepository.findOrderDtos();
    }

v3랑 결과는 똑같은데 select절에서 내가 원하는것만 셀렉트가 됨
-> 쿼리를 할 때 sql 짜듯이 내가 원하는애들만 가져온거
-> 벗 v3는 엔티티를 가져온거라 변경이 가능한데 얘는 DTO로 조회해서 변경이 안된다는 단점이있음

네트워크 상황과 API 상황을 보고 결정을 하자! 어떤 방법을 사용할지!

profile
풀스택 개발자 가보자구~
post-custom-banner

0개의 댓글