@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 = getMemberWidthNameAndAddressProps("user A", "서울", "1", "1245");
em.persist(member);
Book book1 = getBookWidthNamePriceStock("JPA1 bOOK", 10000, 100);
em.persist(book1);
Book book2 = getBookWidthNamePriceStock("JPA2 bOOK", 20000, 200);
em.persist(book2);
OrderItem orderItem1 = OrderItem.createOrderItem(book1, 10000, 1);
OrderItem orderItem2 = OrderItem.createOrderItem(book2, 20000, 2);
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress());
Order order = Order.createOrder(member, delivery, orderItem1, orderItem2);
em.persist(order);
}
public void dbInit2(){
...
}
private static Member getMemberWidthNameAndAddressProps(String name, String cityName, String street, String zipcode) {
...
return member;
}
private static Book getBookWidthNamePriceStock(String name, int price, int stock) {
...
return book;
}
}
}
Spring 이 실행되면서, @PostConstruct
의 함수를 먼저 실행함!
이때 목업/샘플 데이터를 주입해주면 됨!!
지금부터 설명하는 내용은 정말 중요!!
대충 넘어가면 엄청난 시간을 날리고 인생을 허비하게 될 것임!! feat. 김영한 개발자님
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1(){
List<Order> all = orderRepository.findAll(new OrderSearch());
return all;
}
무난하게 List<Order>
를 json 파일로 바꿔줄 것 같은 식이다!
하지만!! json 으로 바꿔주는 과정에서,
<Order.java>
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
member를 타고
<Member.java>
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<Order>();
여기서 orders를 호출하여 이를 다시 json 화 시키는데,
이때 무한 루프가 발생한다!!
양방향 연관관계에서의 문제가 생김!
@JsonIgnore
을 걸어주면 된다!
양방향 중에 한쪽은 무조건 걸어줘야함!! (일대다, 일대일 이던, 양방향이라면)
Member
OrderItem
Delivery
다 바꿔주기
class org.hibernate.proxy.poho.bytebuddy.ByteBuddyInterceptor
하는 친구가 뜬다.
바로,lazy loading
에 걸려있는 객체들이 json화 되는 과정에서 문제가 된 것!
<build.gradle>
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5' // 추가
<mainController> 추가
@Bean
Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
위 처럼, lazy loading
데이터들은 null로 넘어온다!
lazy 로딩을 억지로 불러와서 data를 강제할 수 있는 속성도 추가할 수 있음!
hibernate5Module.configure(Hibernate5Module.Featrue.FORCE_LAZY_LOADING, true);
엔티티를 직접 반환하는 api 는 여러모로 제약이 많다.
json화, lazy loading 등 여러가지를 신경써줘야함.
1. 왠만하면 DTO 로 변환해서 반환하자!!
2. lazy loading 때문에, eager 로 바꾸는 것이 아니라, 필요한 경우 페치 조인으로 ㄱㄱ
덧) 반환 값은 리스트로 보내는 일이 없도록하자!
추가적으로 필드 값을 수정할때, 확장이 용이 하지 않음
<newField 추가 하기 쉽네~>
{
newField: ~~~,
data: [ ... ]
}
<newField 어디에 추가하지..?>
[
...
]
v1 에서의 엔티티 직접 반환을 해결하여, DTO를 세팅하면 아래와 같다.
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
List<Order> orders = orderRepository.findAll(new OrderSearch());
List<SimpleOrderDto> orderDtoList = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return orderDtoList;
}
@Data
static class SimpleOrderDto {
public SimpleOrderDto(Order order) {
this.orderId = order.getId();
this.name = order.getMember().getName(); // LAZY 초기화
this.orderDate = order.getOrderDate();
this.orderStatus = order.getStatus();
this.address = order.getDelivery().getAddress(); // LAZY 초기화
}
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
}
lazy loading
으로 인한, 엔티티 개별로 query 호출!! (영속성 컨텍스트로 호출이긴 함)
n + 1
문제
그렇다고 Lazy -> Eager로 바꾸면? NoNo!
Fetch Join 튜닝!! ㄱㄱ
< OrderRespository.java >
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();
}
Fetch Join 을 활용하여, 애초에 값을 다 가지고오기!
JPA 가 고맙게도 쿼리를 한방에 작성해서 날려줌
<OrderSimpleApiController.java>
@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;
}
v2 와 결과는 같지만! 실행하는 Query 개수가 다름!!
JPA 에서 바로 DTO 로 조회!!
<OrderSimpleApiController.java>
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
return orderRepository.findOrderDtos();
}
<OrderRepository.java>
public List<OrderSimpleQueryDto> findOrderDtos(){
return em.createQuery("select new jpabook.jpashop.repository.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();
}
<OrderSimpleQueryDto.java>
@Data
public class OrderSimpleQueryDto {
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;
}
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
}
차이점이라 하면!
Entity에 종속되어 DTO를 만들지 않고,
Entity와 별개로 원하는 것만 얻어오는 DTO 를 만들 수 있다는 것!!
(member 나 delivery 상세 정보 같은 것들 생략 가능)
- 하지만 재사용성이 떨어짐! 확장성은 거의 없기 때문에
V3 과 V4 는
Trade-Off
가 있음.
가급적 V3 를 추천!!
너무나도 많은 Traffic을 다루는 거라면, 해당 프로세스에 맞게 V4 방식으로 고치면 됨.
김영한 개발자님 실무 팁
repository
에 query api 스펙이 포함된 DTO 파일을 관리하는게, 뭔가 애매 (+ repository 의 return 값이 순수하지 않음)
query 용 repository
를 따로 만드는 것으로 하는 편
<OrderSimpleQueryRepository.java>
@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();
}
}
<OrderSimpleApiController.java>
...
private final OrderSimpleQueryRepository orderSimpleQueryRepository;
...
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
return orderSimpleQueryRepository.findOrderDtos();
}
...
이런 식으로 새로운 repository 를 추가해서, Controller 나 Service 계층에서 사용하는 방식을 선호하심!! (V4의 경우)
조회 방식
1. 엔티티로 받아 DTO로 변환 후 전달 (V3)
-- 리포지토리 재사용성 좋음
-- 개발이 단순해짐
-- fetch join 활용 가능
- DTO로 받아 그대로 전달 (V4)
-- 하게 된다면!
-- 새로운 독립된 repository 를 만들어서, 주입시키는 것을 추천!