1️⃣ 간단한 주문 조회 V1 : 엔티티를 직접 노출
⭐ 요약
- 엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭! 한곳에
@JsonIgnore
처리해야 한다.- 정말 간단한 애플리케이션이 아니면 엔티티를 API 응답으로 외부로 노출하는 것은 좋지 않다. 따라서 DTO로 변환해서 반환하는 것이 더좋은 방법이다.
- 지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안된다!
즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다. 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워 진다.- 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용하자!
- 핵심
- order 조회
- order에서 member와 연관이 걸리게 하는 것
- order에서 delivery와 연관이 걸리게 하는 것
- XToOne
- order - member ManyToOne
- order - delivery OneToOne
api.OrderSimpleApiController
package jpabook.jpashop.api;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
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> orderv1(){
List<Order> all = orderRepository.findAll(new OrderSearch());
return all;
}
}
첫번째 에러
- member의 order로 접근하면 order의 member로 다시 접근하는 식으로 무한 루프에 빠진다
해결법
- order와 관련 있는 객체 클래스로 가서 order 정의 위에
@JsonIgnore
를 붙여주어야 된다.domain.member
... public class Member { ... @JsonIgnore @OneToMany(mappedBy = "member") private List<Order> orders = new ArrayList<>(); }
domain.OrderItem
public class OrderItem { ... @JsonIgnore @ManyToOne(fetch = FetchType.LAZY) // 주문 아이템 입장에서 여러 주문 아이템들은 한 주문에 담기게 된다. @JoinColumn(name = "order_id") private Order order; ... }
domain.Delivery
public class Delivery { ... @JsonIgnore @OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY) private Order order; ... }
두 번째 에러
- Order class에서 member는 지연로딩이기에 member 객체에 접근 전까지는 데이터를 가지고 오지 않는다.
- 이때 하이버네이트는 new proxymember()를 통해 프록시멤버 객체를 생성해서 넣어놓게 된다.
→ bytebuddy- 여기서 json이 루프를 돌려 order를 쭉 뽑아보려고 하는데 member 안에는 진짜 member 객체가 아닌 프록시가 들어가 있어서 에러가 발생하게 된다.
해결법
- 하이버네이트 모듈을 설치해야 한다.
-jpabook.jpashop.JpashopApplication
```java 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(); return hibernate5Module; } } ```
결과
member = null 인 이유는 지연로딩이기 때문이다.
member 데이터와 delivery 데이터를 띄우고 싶다면?
방법 1
jpabook.jpashop.JpaApplication
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(); //강제 지연 로딩 설정 // entity 노출 하면 안 좋고 성능도 좋지 못 함 // hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true); return hibernate5Module; } }
- entity를 직접 노출하는 것
- 성능도 좋지 못하니 쓰지 말 것~.ᐟ.ᐟ
방법 2
api.OrderSimpleApiController
... public class OrderSimpleApiController { private final OrderRepository orderRepository; @GetMapping("/api/v1/simple-orders") public List<Order> orderv1(){ List<Order> all = orderRepository.findAll(new OrderSearch()); for (Order order : all) { // order.getmember() 까지는 프록시 멤버(db에 쿼리가 날리진 않은 상태 // order.getMember().getName() 까지 하면 LAZY 강제 초기화가 된다. order.getMember().getName(); // LAZY 강제 초기화 order.getMember().getAddress(); // LAZY 강제 초기화 } return all; } }
- 엔티티를 직접 노출하지 말 것~.ᐟ.ᐟ
결과
2️⃣ 간단한 주문 조회 V2 : 엔티티를 DTO로 변환
문제점
- v1과 v2 모두 LAZY_LOADING으로 인한 데이터베이스 쿼리가 너무 많이 호출된다.
엔티티를 DTO로 변환하는 일반적인 방법이다.
쿼리가 총 1 + N번 실행된다. (v1과 쿼리 수 결과는 동일)
- order → 조회 1번 → order 조회 결과 수 : N
- order를 통해 member 조회 → 지연 조회 N번
- order를 통해 delivery 조회 → 지연 조회 N번
LAZY 초기화
- : 영속성 컨텍스트가 이 멤버 id를 가지고 영속성 컨텍스트를 찾아온다.
만약 없다면 디비 쿼리를 날린다.
api.OrderSimpleApiController
package jpabook.jpashop.api;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
...
**@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2(){
List<Order> orders = orderRepository.findAll(new OrderSearch()); // orders를 가지고 와서
List<SimpleOrderDto> result = orders.stream() // stream으로 하나하나 For문 거쳐가며
.map(o -> new SimpleOrderDto(o)) // Order 엔티티를 DTO로 바꿔준 다음
.collect(Collectors.toList()); // collect를 이용해 list로 다시 변환해 준다.
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();
}
}**
}
3️⃣ 간단한 주문 조회 V3 : fetch join 최적화
- v2에서 쿼리를 하나하나 날렸다면 V3에서는 fetch join을 이용해 order를 불러오려 할 때 관련된 member나 delivery도 같이 한꺼번에 조회를 해 버린다.
- 재사용이 가능하다
- 엔티티로 조회한 것이다.
api.OrderSimpleApiController
package jpabook.jpashop.api;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
...
@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 초기화 : 영속성 컨텍스트가 이 멤버 id를 가지고 영속성 컨텍스트를 찾아온다. 만약 없다면 디비 쿼리를 날린다.
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
}
}
@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;
}
}
repository.OrderRepository
package jpabook.jpashop.repository;
...
@Repository
@RequiredArgsConstructor
public class OrderRepository {
private final EntityManager em;
...
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();
}
}
4️⃣ 간단한 주문 조회 V4 : JPA에서 DTO로 바로 조회
- 재사용이 불가능하다.
- DTO로 조회한 것이다. → 변경 불가능
- 일반적인 SQL사용 할 때 처럼 원하는 값을 선택해서 조회한다.
- new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환한다.
- 애플리케이션 네트워크 용량이 최적화되나 생각보다 미비하다.
- Repository 재사용성이 떨어진다. API 스펙에 맞춘 코드가 Repository에 들어가는 단점 존재
- repository는 순수하게 엔티티를 조회하는 용도로 사용할 것~.ᐟ.ᐟ
api.OrderSimpleApicontroller
package jpabook.jpashop.api;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryDto;
import jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryRepository;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
private final OrderSimpleQueryRepository orderSimpleQueryRepository;
...
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4(){
return orderSimpleQueryRepository.findOrderDtos();
}
}
repository.OrderRepository
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.Order;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class OrderRepository {
private final EntityManager em;
...
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();
}
}
repository.order.simplequery.OrderSimpleQueryRepository
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();
}
}
repository.order.simplequery.OrderSimpleDto
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; // LAZY 초기화 : 영속성 컨텍스트가 이 멤버 id를 가지고 영속성 컨텍스트를 찾아온다. 만약 없다면 디비 쿼리를 날린다.
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
}
}
정리
- 엔티티를 조회하여 DTO로 변환하기 → V3
- 리포지토리 재사용성 좋고 개발도 단순해진다.
- DTO로 바로 조회하기 → V4
권장하는 방법
- 엔티티를 DTO로 변환하는 방법을 선택한다. → V2
- 필요하면 fetch join으로 성능을 최적화한다. → V3
- 그래도 안 되면 DTO로 직접 조회하는 방법을 사용한다. → V4
- 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서
SQL을 직접 사용한다.