반응형
조회용 샘플 데이터 추가
- 다음과과 같이 샘플 데이터를 만들어두자.
- userA 는
JPA1 BOOK1
,JPA2 BOOK2
주문 - userB 는
SPRING1 BOOK
,SPRING2 BOOK
주문
- userA 는
package jpabook.jpashop;
import jpabook.jpashop.domain.*;
import jpabook.jpashop.domain.item.Book;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
@Component
@RequiredArgsConstructor
public class InitDb {
private final InitService initService;
@PostConstruct
public void init() {
initService.dbInit1();
initService.dbInit2();
}
@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);
Order order = Order.createOrder(member, createDelivery(member),
orderItem1, orderItem2);
em.persist(order);
}
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);
Delivery delivery = createDelivery(member);
OrderItem orderItem1 = OrderItem.createOrderItem(book1, 20000, 3);
OrderItem orderItem2 = OrderItem.createOrderItem(book2, 40000, 4);
Order order = Order.createOrder(member, delivery, orderItem1,
orderItem2);
em.persist(order);
}
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;
}
private Book createBook(String name, int price, int stockQuantity) {
Book book = new Book();
book.setName(name);
book.setPrice(price);
book.setStockQuantity(stockQuantity);
return book;
}
private Delivery createDelivery(Member member) {
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress());
return delivery;
}
}
}
- 조회용 샘플 데이터 소스코드는 다음과 같다.
지연 로딩과 조회 성능 최적화
- 주문 + 배송정보 + 회원을 조회하는 API를 만들어보며, 지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해보자.
주문 조회 V1
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderSearch;
import jpabook.jpashop.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@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().getOrders(); // Lazy 강제 초기화
order.getDelivery().getAddress(); // Lazy 강제 초기화
}
return all;
}
}
- V1 상황 :
DTO변환
을 사용하지 않은 경우Order
는 양방향연관관계로 지연 로딩 설정이 되어 있다. 따라서 실제 엔티티가 아닌 프록시로 존재한다. 현재 상황에서 조회를 하게되면, 무한루프 (서로의 컬렉션이 계속 조회)가 발생한다.- 양방향 연관관계에서 한 쪽에
@JsonIgnore
설정을 해줘야한다. jackson
라이브러리는 프록시 객체를 json으로 생성하는 방법을 모르기 때문에Hibernate5Module
을 스프링 빈으로 등록하여 초기화 된 프록시 객체만 노출 시켜야한다.
@JsonIgnore
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
- 다음과 같이 양방향 연관관계에서 한 쪽을
@JsonIgnore
설정을 해줘야 한다.
Hibernate5Module (참고)
Hibernate5Module
의 방법은 DTO가 아닌 실제 엔티티를 반환하기 위해 사용하는 라이브러리이다. 실무에선 DTO를 사용하여 반환하기 때문에 해당 내용은 참고만 하자.
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
- build.gradle에 라이브러리 추가
package jpabook.jpashop;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class JpashopApplication {
public static void main(String[] args) {
SpringApplication.run(JpashopApplication.class, args);
}
@Bean
Hibernate5Module hibernate5Module() {
Hibernate5Module hibernate5Module = new Hibernate5Module();
// hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true);
return hibernate5Module;
}
}
Hibernate5Module
를 빈으로 등록한다. 주석 처리를 한 부분에서 강제로 지연 로딩 설정을 할 수 있다.
양방향 연관관계인 경우 무한 루프 방지를 위해 한 곳을
@JsonIgnore
처리 해야 한다.지연 로딩을 피하기 위해 즉시 로딩으로 설정하면 안된다. (오히려 성능 상의 문제 발생 및 성능 튜닝이 매우 어려워진다.)
지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우엔 페치 조인을 사용하자.
주문 조회 V2
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
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 초기화 - 영속성컨텍스트에 없으므로 Member를 조회 후 저장
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); // LAZY 초기화
}
}
- 조회 결과를 위한 DTO를 생성했다. 하지만 로그에 찍힌 쿼리 내용을 보면 이상한 점이 있다.
- 로그 내용 : order 조회 - member 조회 - delivery 조회 - member 조회 - delivery 조회
- LAZY 초기화 되는 부분에서 member 와 delivery 조회는 당연하다. 하지만 존재하는 데이터 갯수만큼 반복적으로 LAZY 초기화하는 것을 볼 수 있다. (N+1 문제 발생)
주문 조회 V3
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();
}
- 페치 조인을 적용하기 위해
OrderRepository
에 해당 메서드를 추가하자.
@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;
}
- 페치 조인을 적용해서 로그를 확인해보면 하나의 쿼리문으로 성능이 최적화 된 것을 확인 할 수 있다.
주문 조회 V4
package jpabook.jpashop.repository.order.simplequery;
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;
}
}
- 필요한 데이터를 사용하기 위해 별도의 DTO를 만든다.
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
return orderSimpleQueryRepository.findOrderDtos();
}
- DTO 형식의 리스트를 반환하도록 만든다.
package jpabook.jpashop.repository.order.simplequery;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import java.util.List;
@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();
}
}
- 엔티티가 아닌 DTO를 반환하는 레포지토리는 별도로 생성하자.
- 레포지토리는 엔티티를 조회하는 용도로 사용되며, 성능 최적화를 위해 DTO를 사용한다면 별도의 레포지토리를 생성하는 것이 올바르다.
- DTO를
select
하기 위해new
연산자를 사용해서 불러온다.
V3 vs V4
성능 상으로는 V4가 좋지만, 리포지토리의 재사용성이 떨어지고 API 스펙에 맞춘 코드가 리포지토리에 들어가게 된다.
<쿼리 방식 선택 권장 순서>
DTO 변환 - V2
필요하면 페치 조인으로 성능 최적화 (대부분의 이슈 해결) - V3
해결이 안된 경우 DTO로 직접 조회하는 방법 사용 - V4
최후의 방법으로 네이티브 SQL 이나 JDBC Template을 사용해서 직접 SQL 사용
본 포스팅은 인프런 김영한님 강의(실전! 스프링 부트와 JPA 활용2)를 토대로 정리한 내용입니다.
반응형