Spring Boot와 JPA 활용2 - TIL(2)

YulHee Kim·2021년 10월 3일
0

Spring Boot와 JPA 활용

목록 보기
9/11

[참고 강의] 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

💡 API 개발 고급 - 준비

샘플 데이터를 입력해보겠다.

  • userA
    • JPA1 BOOK
    • JPA2 BOOK
  • userB
    • SPRING1 BOOK
    • SPRING2 BOOK

✏️ 조회용 샘플 데이터 입력

@Component
@RequiredArgsConstructor
public class InitDb {

    private final InitService initService;

    @PostConstruct
    public void init() {
        initService.dbInit1();
    }

    @Component
    @Transactional
    @RequiredArgsConstructor
    static class InitService {

        private final EntityManager em;

        public void dbInit1() {
            Member member = createMember("userA", "서울","1","1111");
            em.persist(member);

            Book book1 = createBook("JPA1 BOOK", 10000, 100);
            em.persist(book1);

            Book book2 = createBook("JPA2 BOOK", 20000, 100);
            em.persist(book2);

            OrderItem orderItem1 = OrderItem.createOrderItem(book1, 10000, 1);
            OrderItem orderItem2 = OrderItem.createOrderItem(book2, 20000, 2);

            Delivery delivery = createDelivery(member);
            Order order = Order.createOrder(member, delivery, orderItem1, orderItem2);
            em.persist(order);
        }

        private Delivery createDelivery(Member member) {
            Delivery delivery = new Delivery();
            delivery.setAddress(member.getAddress());
            return delivery;
        }

        private Book createBook(String name, int price, int stockQuantity) {
            Book book1 = new Book();
            book1.setName(name);
            book1.setPrice(price);
            book1.setStockQuantity(stockQuantity);
            return book1;
        }

        private Member createMember(String name, String city, String street, String zipcode) {
            Member member = new Member();
            member.setName(name);
            member.setAddress(new Address(city, street, zipcode));
            return member;
        }

        public void dbInit2() {
            Member member = createMember("userB", "진주", "2","2222");
            em.persist(member);

            Book book1 = createBook("SPRING1 BOOK", 20000, 200);
            em.persist(book1);

            Book book2 = createBook("SPRING2 BOOK", 40000, 300);
            em.persist(book2);

            OrderItem orderItem1 = OrderItem.createOrderItem(book1, 20000, 3);
            OrderItem orderItem2 = OrderItem.createOrderItem(book2, 40000, 4);

            Delivery delivery = createDelivery((Member) member);
            Order order = Order.createOrder(member, delivery, orderItem1, orderItem2);
            em.persist(order);
        }
    }
}

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

주문 + 배송 정보 + 회원을 조회하는 API를 만들자
지연 로딩에 의해 발생하는 성능 문제를 단계적으로 해결해보겠다.

✏️ 간단한 주문 조회: 엔티티를 직접 노출

OrderSimpleApiController

/**
 * xToOne(ManyToOne, OneToOne)
 * Order
 * Order -> Member
 * Order -> Delivery
 */
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;

    @GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for (Order order : all) {
            order.getMember().getName(); //LAZY 강제 초기화
            order.getDelivery().getAddress(); //LAZY 강제 초기화
        }
        return all;
    }
}
  • 엔티티를 직접 노출하는 것은 좋지 않다.
  • order -> memberorder -> address 는 지연로딩이다. 따라서 실제 엔티티 대신에 프록시 존재한다.
  • jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모름 -> 예외 발생
  • Hibernate5Module을 스프링 빈으로 등록하면 해결

Hibernate5Module 등록

JsonshopApplication에 다음 코드를 추가

    @Bean
    Hibernate5Module hibernate5Module() {
        return new Hibernate5Module();
    }
  • 기본적으로 초기화 된 프록시 객체만 노출, 초기화 되지 않은 프록시 객체는 노출 안함
  • 엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭! 한곳을 @JsonIgnore 처리 해야 한다. 안그러면 양쪽을 서로 호출하면서 무한 루프가 걸린다.

주의)
지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EAGER)으로 설정하면 안된다. 즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다.
항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 fetch join을 사용해야한다.

✏️ 엔티티를 DTO로 변환

    @GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2() {
        //ORDER 2개
        //N + 1 -> 1 + 회원 N, 배송 N
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        List<SimpleOrderDto> result = orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());

        return result;
    }
    
    @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(); //LAZY 초기화
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress(); //LAZY 초기화
        }
    }
  • 엔티티를 DTO로 변환하는 일방적인 방법
  • 쿼리가 총 1 + N + N 번 실행된다.
    - order 조회 1번
    • order -> member 지연 로딩 조회 N번
    • order -> delivery 지연 로딩 조회 N번
    • 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.

✏️ 엔티티를 DTO로 변환 - 페치 조인 최적화

    @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;
    }

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();
    }
  • 엔티티를 fetch join을 사용해서 쿼리 1번에 조회
  • fetch join으로 order -> member, order -> delivery는 이미 조회된 상태이므로 지연로딩 X

profile
백엔드 개발자

0개의 댓글

관련 채용 정보