API 개발 고급 설명을 위해 샘플 데이터를 입력하자😃
✔️ InitDB
클래스 생성
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;
/**
* 총 주문 2개
* * 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();
initService.dbInit2();
}
@Component
@Transactional
@RequiredArgsConstructor
static class InitService {
private final EntityManager em;
public void dbInit1() {
System.out.println("Init1" + this.getClass());
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);
}
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);
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 book1 = new Book();
book1.setName(name);
book1.setPrice(price);
book1.setStockQuantity(stockQuantity);
return book1;
}
private Delivery createDelivery(Member member) {
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress());
return delivery;
}
}
}
주문 + 배송정보 + 회원을 조회하는 API를 만들어 보자☺️
지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해 볼 것이다!
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
/**
* V1. 엔티티 직접 노출
* - Hibernate5Module 모듈 등록, LAZY=null 처리 * - 양방향 관계 문제 발생 -> @JsonIgnore
*/
@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;
}
}
🤚🏻 양방향 연관관계가 있을 땐, 한 쪽에
@JsonIgnore
추가
안 그러면 양쪽을 서로 호출하면서 무한 루프가 걸리게 된다! 조심!
order
➡️ member
와 order
➡️ address
는 지연 로딩이다.jackson
라이브러리는 기본적으로 이 프록시 객체를 json
으로 어떻게 생성해야 하는지 모름 ➡️ Hibernate5Module
을 스프링 빈으로 등록하면 해결!!(스프링 부트 사용중)📌
build.gradle
에 다음 라이브러리 추가implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
📌 Hibernate5Module 등록
JpashopApplication 에
다음 코드를 추가!@Bean Hibernate5Module hibernate5Module() { return new Hibernate5Module(); }
➡️ 이렇게 설정하면, 기본적으로 초기화 된 프록시 객체만 노출하고 초기화 되지 않은 프록시 객체는 노출 안함!
1️⃣ 엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭! 한곳을 @JsonIgnore
처리 해야 한다.
2️⃣ 앞에서 계속 강조했듯이 정말 간단한 애플리케이션이 아니면 엔티티를 API 응답으로 외부로 노출하는 것은 좋지 않다🙁
➡️ 따라서 Hibernate5Module
를 사용하기 보다는 DTO
로 변환해서 반환하는 것이 더 좋은 방법이다!
3️⃣ 지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안된다!
➡️ 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join
)을 사용하자!
/**
* V2. 엔티티를 조회해서 DTO로 변환(fetch join 사용X) * - 단점: 지연로딩으로 쿼리 N번 호출
*/
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<SimpleOrderDto> result = orders.stream()
// order를 SimpleOrderDto로 바꿔치기
.map(o -> new SimpleOrderDto(o))
.collect(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();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
}
}
order
조회 1번(order
조회 결과 수가 N이 됨) order ➡️ member
지연 로딩 조회 N번order ➡️ delivery
지연 로딩 조회 N번order
의 결과가 4개면 최악의 경우 1 + 4 + 4번 실행된다.(최악의 경우)초쿰,,, 많이,,,, 어렵네요,,,,,