[참고 강의] 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화
샘플 데이터를 입력해보겠다.
@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를 만들자
지연 로딩에 의해 발생하는 성능 문제를 단계적으로 해결해보겠다.
/**
* 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
-> member
와 order
-> address
는 지연로딩이다. 따라서 실제 엔티티 대신에 프록시 존재한다.JsonshopApplication에 다음 코드를 추가
@Bean
Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
주의)
지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EAGER)으로 설정하면 안된다. 즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다.
항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 fetch join을 사용해야한다.
@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 초기화
}
}
@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;
}
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();
}