<aside> ❗
@Component
@RequiredArgsConstructor
public class InitDB {
private final InitService initService;
@PostConstruct
public void init() {
initService.dbInit1();
initService.dbInit2();
}
// init 메서드에 코드를 추가하지 않고 서로 다른 빈으로 등록해서 호출하는 이유
// @Transactional으로 인해 정상적으로 동작 안 할 수 있음
@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.creatOrder(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.creatOrder(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;
}
}
}
<aside> ❗
순환참조 문제가 발생
**한다.양방향 연관관계의 경우 한 쪽의 필드에 @JsonIgnore 애너테이션을 붙여줘야 한다.
******ByteBuddyinterceptor
객체가 대신 연관관계 필드에 들어가 있음(실제 엔티티 객체 X)JSON이 프록시 객체에 접근하여 데이터를 가져올 수 없어서 예외 발생
[해결책]
스프링 부트 3.x 이상
은 아래의 코드를 build.gradle에 저장해야 한다.implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5-jakarta'
JpaShopApplication
에 아래 코드를 추가하자.@Bean
Hibernate5JakartaModule hibernate5Module() {
return new Hibernate5JakartaModule();
}
기본적으로 초기화 된 프록시 객체만 노출
, 초기화되지 않은 프록시 객체는 노출하지 않음@Bean
Hibernate5JakartaModule hibernate5Module() {
Hibernate5JakartaModule hibernate5JakartaModule = new Hibernate5JakartaModule();
hibernate5JakartaModule.configure(Hibernate5JakartaModule.Feature.FORCE_LAZY_LOADING, true);
return hibernate5JakartaModule;
}
조회 쿼리가 추가로 발생
**한다.@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByString(new OrderSearch());
for (Order order : all) {
// 프록시 객체를 초기화해서 실제 엔티티를 가져오도록 함
order.getMember().getName();
order.getDelivery().getAddress();
}
return all;
}
그러므로 DTO로 변환해서 사용하자
연관관계가 필요 없는 경우에도 항상 조회해서 성능 문제가 발생
할 수 있다.
→ 성능 최적화 여지가 확 줄어든다.
성능 최적화가 필요한 경우 패치 조인을 사용해라
</aside>
<aside> ❗
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
return orders.stream()
.map(o -> new SimpleOrderDto(o))
.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) {
// 필요한 API 스펙만 만들어서 반환하면 된다.
this.orderId = order.getId();
this.name = order.getMember().getName();
this.orderDate = order.getOrderDate();
this.orderStatus = order.getOrderStatus();
this.address = order.getDelivery().getAddress();
}
}
[문제점]
쿼리가 총 N + 1 번 실행
**된다. (v1과 쿼리 결과수는 동일)
<aside> ❗
프록시 객체는 JSON으로 변환될 수 없음
</aside>
<aside> ❗
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();
}
페치 조인을 사용해서 쿼리 1번에 조회
// 결과는 V2와 동일지연로딩 X
Lazy 로딩이 걸려있어도** **fetch join이 우선권**
을 갖는다.
즉, 프록시 객체가 아니라 모두 실제 엔티티를 하나의 쿼리를 이용해 데이터를 가져온다.[문제점]
<aside> ❗
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> orderV4() {
return orderRepository.findOrderDtos();
}
List<Order>를 List<OrderDto>로 변환하는 과정 생략
// API 스펙에 맞춘 코드가 리포지토리에 들어가게 됨 (논리적으로 계층이 깨져 있다.)
// API 스펙이 바뀌면 해당 코드를 고쳐야 하는 문제 발생
// OrderRepository에 넣는게 아니라 리포지토리 패키지 내에 하위 패키지를 만든다.
// repository.order.simplequery
public List<OrderSimpleQueryDto> findOrderDtos() {
return em.createQuery(
"select new jpabook.jpashop.repository.OrderSimpleQueryDto(o.id, o.member.name, o.orderDate, o.orderStatus, d.address) from Order o " +
" join o.member m " +
" join o.delivery d ", OrderSimpleQueryDto.class)
.getResultList();
}
일반적인 SQL을 사용할 때 처럼 원하는 값을 선택해서 조회
new
명령어를 사용해서, JPQL의 결과를 DTO로 즉시 반환
SELECT 절에서 원하는 데이터를 직접 선택하므로 DB → 애플리케이션 네트쿽 용량 최적화 (생각보다 미비)
리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점
→ orderRepository는 순수한 엔티티를 조회하는데 사용해야 함
복잡한 조인 쿼리를 가지고 DTO를 뽑아야 할 때, 쿼리 서비스, 쿼리 리포지토리로 뽑으면 유지보수성이 높아진다. → 리포지토리 패키지에 하위 패키지에 리포지토리를 만들어서 거기에 작성 (권장) → order.simplequery/OrderSimpleQueryRepository
또한 쿼리 검색 결과를 DTO에 바로 담아서 반환해줌으로써, 변환 과정을 생략할 수 있음
단, 생성자에 객체를 넘길 수 없음, 그러므로 필요한 매개변수에 적절하게 넣어야 함
@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) {
// 필요한 API 스펙만 만들어서 반환하면 된다.
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
}
}
페치조인
- 엔티티 자체를 검색할 때, 내부에 필요한 부분만 함께 조인해 가져오는 것
엔티티 자체를 가져오기 때문에 여러 곳에서 사용 가능DTO 바로 조회
- 사실상 SQL 쿼리를 작성하여 가져온 것, 그러므로 재사용성이 떨어짐
DTO에 지정된 값만 가져오기 때문에 재사용성이 떨어짐
코드 가독성이 떨어짐데이터 크기가 클 경우는 고민해봐야 한다.
</aside><aside> ❗