본 글은 인프런의 김영한님 강의 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화
을 수강하며 기록한 필기 내용을 정리한 글입니다.
-> 인프런
-> 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 강의
Order : OrderItem = 1 : N
, OrderItem : Item = N : 1
관계일 때, Order 엔티티에서 OrderItem과 Item을 조회한다.@~~ToMany
어노테이션이 적용되어 있는 컬렉션 필드 조회 최적화쓰지말자
@GetMapping("/order/v2")
public OrderResponseFormat<List<OrderResponseDto>> ordersV2() {
return new OrderResponseFormat<>("주문 상세 조회 완료",
orderService.findOrders(new OrderSearch())
.stream()
.map(OrderResponseDto::new)
.collect(Collectors.toList()));
}
@Data
@AllArgsConstructor
static class OrderResponseFormat<T> {
String message;
T Data;
}
@Data
static class OrderResponseDto {
private Long orderId;
private MemberResponseDto member;
private List<OrderItemResponseDto> orderItems = new ArrayList<>();
private DeliveryResponseDto delivery;
private LocalDateTime orderDate;
private OrderStatus status;
public OrderResponseDto(Order order) {
this.orderId = order.getId();
this.member = new MemberResponseDto(order.getMember());
this.orderItems = order.getOrderItems()
.stream()
.map(OrderItemResponseDto::new)
.collect(Collectors.toList());
this.delivery = new DeliveryResponseDto(order.getDelivery());
this.orderDate = order.getOrderDate();
this.status = order.getStatus();
}
}
@Data
static class MemberResponseDto {
private Long memberId;
private String name;
public MemberResponseDto(Member member) {
this.memberId = member.getId();
this.name = member.getName();
}
}
@Data
static class DeliveryResponseDto {
private Long deliveryId;
private Address address;
private DeliveryStatus status;
public DeliveryResponseDto(Delivery delivery) {
this.deliveryId = delivery.getId();
this.address = delivery.getAddress();
this.status = delivery.getStatus();
}
}
@Data
static class OrderItemResponseDto {
private Long orderItemId;
private ItemResponseDto itemInfo;
private Integer orderPrice;
private Integer count;
public OrderItemResponseDto(OrderItem orderItem) {
this.orderItemId = orderItem.getId();
this.itemInfo = new ItemResponseDto(orderItem.getItem());
this.orderPrice = orderItem.getOrderPrice();
this.count = orderItem.getCount();
}
}
@Data
static class ItemResponseDto {
private Long itemId;
private String name;
public ItemResponseDto(Item item) {
this.itemId = item.getId();
this.name = item.getName();
}
}
@Override
public List<Order> findAllWithItem(OrderSearch orderSearch) {
return em.createQuery(
"SELECT o FROM Order o "
+ "JOIN FETCH o.member "
+ "JOIN FETCH o.delivery d "
+ "JOIN FETCH o.orderItems oi "
+ "JOIN FETCH oi.item i", Order.class)
.getResultList();
}
따라서 다음과 같이 응답이 출력되어 버린다.
{
"message": "주문 상세 조회 완료",
"data": [
{
"orderId": 4,
"member": {
"memberId": 1,
"name": "userA"
},
"orderItems": [
{
"orderItemId": 6,
"itemInfo": {
"itemId": 2,
"name": "JPA1 BOOK"
},
"orderPrice": 10000,
"count": 1
},
{
"orderItemId": 7,
"itemInfo": {
"itemId": 3,
"name": "JPA2 BOOK"
},
"orderPrice": 20000,
"count": 2
}
],
"delivery": {
"deliveryId": 5,
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"status": null
},
"orderDate": "2023-07-03T03:11:06.601545",
"status": "ORDER"
},
{
"orderId": 4,
"member": {
"memberId": 1,
"name": "userA"
},
"orderItems": [
{
"orderItemId": 6,
"itemInfo": {
"itemId": 2,
"name": "JPA1 BOOK"
},
"orderPrice": 10000,
"count": 1
},
{
"orderItemId": 7,
"itemInfo": {
"itemId": 3,
"name": "JPA2 BOOK"
},
"orderPrice": 20000,
"count": 2
}
],
"delivery": {
"deliveryId": 5,
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"status": null
},
"orderDate": "2023-07-03T03:11:06.601545",
"status": "ORDER"
},
{
"orderId": 11,
"member": {
"memberId": 8,
"name": "userB"
},
"orderItems": [
{
"orderItemId": 13,
"itemInfo": {
"itemId": 9,
"name": "SPRING1 BOOK"
},
"orderPrice": 20000,
"count": 3
},
{
"orderItemId": 14,
"itemInfo": {
"itemId": 10,
"name": "SPRING2 BOOK"
},
"orderPrice": 40000,
"count": 4
}
],
"delivery": {
"deliveryId": 12,
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"status": null
},
"orderDate": "2023-07-03T03:11:06.634574",
"status": "ORDER"
},
{
"orderId": 11,
"member": {
"memberId": 8,
"name": "userB"
},
"orderItems": [
{
"orderItemId": 13,
"itemInfo": {
"itemId": 9,
"name": "SPRING1 BOOK"
},
"orderPrice": 20000,
"count": 3
},
{
"orderItemId": 14,
"itemInfo": {
"itemId": 10,
"name": "SPRING2 BOOK"
},
"orderPrice": 40000,
"count": 4
}
],
"delivery": {
"deliveryId": 12,
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"status": null
},
"orderDate": "2023-07-03T03:11:06.634574",
"status": "ORDER"
}
]
}
이 때 JPQL에 DISTINCT
키워드를 붙이면 중복되는 데이터를 줄일 수 있다.
OrderRepositoryImpl
@Override
public List<Order> findAllWithItem(OrderSearch orderSearch) {
return em.createQuery(
"SELECT DISTINCT o FROM Order o "
+ "JOIN FETCH o.member "
+ "JOIN FETCH o.delivery d "
+ "JOIN FETCH o.orderItems oi "
+ "JOIN FETCH oi.item i", Order.class)
.getResultList();
}
응답 결과
{
"message": "주문 상세 조회 완료",
"data": [
{
"orderId": 4,
"member": {
"memberId": 1,
"name": "userA"
},
"orderItems": [
{
"orderItemId": 6,
"itemInfo": {
"itemId": 2,
"name": "JPA1 BOOK"
},
"orderPrice": 10000,
"count": 1
},
{
"orderItemId": 7,
"itemInfo": {
"itemId": 3,
"name": "JPA2 BOOK"
},
"orderPrice": 20000,
"count": 2
}
],
"delivery": {
"deliveryId": 5,
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"status": null
},
"orderDate": "2023-07-03T03:12:57.728468",
"status": "ORDER"
},
{
"orderId": 11,
"member": {
"memberId": 8,
"name": "userB"
},
"orderItems": [
{
"orderItemId": 13,
"itemInfo": {
"itemId": 9,
"name": "SPRING1 BOOK"
},
"orderPrice": 20000,
"count": 3
},
{
"orderItemId": 14,
"itemInfo": {
"itemId": 10,
"name": "SPRING2 BOOK"
},
"orderPrice": 40000,
"count": 4
}
],
"delivery": {
"deliveryId": 12,
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"status": null
},
"orderDate": "2023-07-03T03:12:57.761936",
"status": "ORDER"
}
]
}
다음과 같이 데이터 중복이 제거된다.
해당 기능은 DB에 쿼리를 보낼 때 distinct
키워드를 붙이기도 하지만, JPA 자체적으로 중복을 제거하는 기능도 제공해준다.
즉, 만약 DB에서 join한 결과가 완전히 똑같은 row는 중복이 제거되고(원래 DB의 distinct
기능), 이와 더불어 조회된 메인 엔티티(Order) 데이터 중 똑같은 식별자(pk)를 가진 중복 데이터들은 제거해서 반환해준다.
DB에서 중복 제거로 조회 해오고, 애플리케이션 단에서 한번 더 중복을 제거하는 것.
이렇게 구현하면 쿼리가 한번만 나가는 것을 볼 수 있다.
select
distinct order0_.order_id as order_id1_9_0_,
member1_.member_id as member_i1_6_1_,
delivery2_.delivery_id as delivery1_3_2_,
orderitems3_.order_item_id as order_it1_8_3_,
item4_.item_id as item_id2_4_4_,
order0_.created_at as created_2_9_0_,
order0_.modified_at as modified3_9_0_,
order0_.delivery_id as delivery6_9_0_,
order0_.member_id as member_i7_9_0_,
order0_.orderdate as orderdat4_9_0_,
order0_.status as status5_9_0_,
member1_.created_at as created_2_6_1_,
member1_.modified_at as modified3_6_1_,
member1_.city as city4_6_1_,
member1_.street as street5_6_1_,
member1_.zipcode as zipcode6_6_1_,
member1_.name as name7_6_1_,
delivery2_.created_at as created_2_3_2_,
delivery2_.modified_at as modified3_3_2_,
delivery2_.city as city4_3_2_,
delivery2_.street as street5_3_2_,
delivery2_.zipcode as zipcode6_3_2_,
delivery2_.status as status7_3_2_,
orderitems3_.created_at as created_2_8_3_,
orderitems3_.modified_at as modified3_8_3_,
orderitems3_.count as count4_8_3_,
orderitems3_.item_id as item_id6_8_3_,
orderitems3_.order_id as order_id7_8_3_,
orderitems3_.orderprice as orderpri5_8_3_,
orderitems3_.order_id as order_id7_8_0__,
orderitems3_.order_item_id as order_it1_8_0__,
item4_.created_at as created_3_4_4_,
item4_.modified_at as modified4_4_4_,
item4_.name as name5_4_4_,
item4_.price as price6_4_4_,
item4_.stockquantity as stockqua7_4_4_,
item4_1_.artist as artist1_0_4_,
item4_1_.etc as etc2_0_4_,
item4_2_.author as author1_1_4_,
item4_2_.isbn as isbn2_1_4_,
item4_3_.actor as actor1_7_4_,
item4_3_.director as director2_7_4_,
item4_.dtype as dtype1_4_4_
from
orders order0_
inner join
member member1_
on order0_.member_id=member1_.member_id
inner join
delivery delivery2_
on order0_.delivery_id=delivery2_.delivery_id
inner join
order_item orderitems3_
on order0_.order_id=orderitems3_.order_id
inner join
item item4_
on orderitems3_.item_id=item4_.item_id
left outer join
album item4_1_
on item4_.item_id=item4_1_.item_id
left outer join
book item4_2_
on item4_.item_id=item4_2_.item_id
left outer join
movie item4_3_
on item4_.item_id=item4_3_.item_id
⭐ 하지만 컬렉션을 페치 조인 할 경우, 페이징이 불가능해진다. ⭐
위 코드에서 다음과 같이 페이징을 적용할 경우,
@Override
public List<Order> findAllWithItem(OrderSearch orderSearch) {
return em.createQuery(
"SELECT DISTINCT o FROM Order o "
+ "JOIN FETCH o.member "
+ "JOIN FETCH o.delivery d "
+ "JOIN FETCH o.orderItems oi "
+ "JOIN FETCH oi.item i", Order.class)
.setFirstResult(1)
.setMaxResults(100)
.getResultList();
}
다음과 같이 결과는 잘 나온다.
{
"message": "주문 상세 조회 완료",
"data": [
{
"orderId": 11,
"member": {
"memberId": 8,
"name": "userB"
},
"orderItems": [
{
"orderItemId": 13,
"itemInfo": {
"itemId": 9,
"name": "SPRING1 BOOK"
},
"orderPrice": 20000,
"count": 3
},
{
"orderItemId": 14,
"itemInfo": {
"itemId": 10,
"name": "SPRING2 BOOK"
},
"orderPrice": 40000,
"count": 4
}
],
"delivery": {
"deliveryId": 12,
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"status": null
},
"orderDate": "2023-07-03T03:21:54.991072",
"status": "ORDER"
}
]
}
하지만 쿼리를 자세히 보면,
select
distinct order0_.order_id as order_id1_9_0_,
member1_.member_id as member_i1_6_1_,
delivery2_.delivery_id as delivery1_3_2_,
orderitems3_.order_item_id as order_it1_8_3_,
item4_.item_id as item_id2_4_4_,
order0_.created_at as created_2_9_0_,
order0_.modified_at as modified3_9_0_,
order0_.delivery_id as delivery6_9_0_,
order0_.member_id as member_i7_9_0_,
order0_.orderdate as orderdat4_9_0_,
order0_.status as status5_9_0_,
member1_.created_at as created_2_6_1_,
member1_.modified_at as modified3_6_1_,
member1_.city as city4_6_1_,
member1_.street as street5_6_1_,
member1_.zipcode as zipcode6_6_1_,
member1_.name as name7_6_1_,
delivery2_.created_at as created_2_3_2_,
delivery2_.modified_at as modified3_3_2_,
delivery2_.city as city4_3_2_,
delivery2_.street as street5_3_2_,
delivery2_.zipcode as zipcode6_3_2_,
delivery2_.status as status7_3_2_,
orderitems3_.created_at as created_2_8_3_,
orderitems3_.modified_at as modified3_8_3_,
orderitems3_.count as count4_8_3_,
orderitems3_.item_id as item_id6_8_3_,
orderitems3_.order_id as order_id7_8_3_,
orderitems3_.orderprice as orderpri5_8_3_,
orderitems3_.order_id as order_id7_8_0__,
orderitems3_.order_item_id as order_it1_8_0__,
item4_.created_at as created_3_4_4_,
item4_.modified_at as modified4_4_4_,
item4_.name as name5_4_4_,
item4_.price as price6_4_4_,
item4_.stockquantity as stockqua7_4_4_,
item4_1_.artist as artist1_0_4_,
item4_1_.etc as etc2_0_4_,
item4_2_.author as author1_1_4_,
item4_2_.isbn as isbn2_1_4_,
item4_3_.actor as actor1_7_4_,
item4_3_.director as director2_7_4_,
item4_.dtype as dtype1_4_4_
from
orders order0_
inner join
member member1_
on order0_.member_id=member1_.member_id
inner join
delivery delivery2_
on order0_.delivery_id=delivery2_.delivery_id
inner join
order_item orderitems3_
on order0_.order_id=orderitems3_.order_id
inner join
item item4_
on orderitems3_.item_id=item4_.item_id
left outer join
album item4_1_
on item4_.item_id=item4_1_.item_id
left outer join
book item4_2_
on item4_.item_id=item4_2_.item_id
left outer join
movie item4_3_
on item4_.item_id=item4_3_.item_id
마지막에 limit 옵션이 없는 것을 확인할 수 있다.
또한, 다음과 같이 WARN 로그가 찍힌다.
즉, DB에서는 결과 그대로 가져오고, 해당 결과를 메모리에 올려놓고 애플리케이션 단에서 내부적으로 페이징 로직이 이루어지게 된다.
이렇게 되면 DB 결과가 매우 클 경우, 메모리가 초과되어 버리거나 성능적으로 매우 부정적인 영향을 끼칠 것이다.
또한 뻥튀기 되어버린 결과를 기준으로 페이징이 일어나게 된다.
⭐ 따라서 일대다 컬렉션 페치 조인에서는 페이징 기능을 쓰면 안된다. ⭐
@~~ToOne
) 페치 조인은 페이징 가능하다!@~~ToMany
) 컬렉션 페치 조인이 페이징 불가능하다.@~~ToOne
엔티티를 페치조인하여 결과를 가져온다.@~~ToOne
관계인 Member와 Delivery를 페치조인해서 결과를 가져온다.@~~ToOne
관계만 페치 조인하는 경우는 페이징 처리가 가능하다.@~~ToMany
관계인 OrderItem은 getOrderItem().getId() 와 같이 지연 로딩으로 가져온다.hibernate.default_batch_fetch_size
application.yml
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/jpashop
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
# show_sql: true
format_sql: true
default_batch_fetch_size: 100
logging.level:
org.hibernate.SQL: debug
org.hibernate.type: trace #스프링 부트 2.x, hibernate5
# org.hibernate.orm.jdbc.bind: trace #스프링 부트 3.x, hibernate6
@BatchSize
Order
...
@BatchSize(size = 500)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
...
... WHERE order_id in (?, ?, ?, ...)
... WHERE item_id in (?, ?, ?, ...)
@~~ToOne
관계만 페치조인함 select
order0_.order_id as order_id1_9_0_,
member1_.member_id as member_i1_6_1_,
delivery2_.delivery_id as delivery1_3_2_,
order0_.created_at as created_2_9_0_,
order0_.modified_at as modified3_9_0_,
order0_.delivery_id as delivery6_9_0_,
order0_.member_id as member_i7_9_0_,
order0_.orderdate as orderdat4_9_0_,
order0_.status as status5_9_0_,
member1_.created_at as created_2_6_1_,
member1_.modified_at as modified3_6_1_,
member1_.city as city4_6_1_,
member1_.street as street5_6_1_,
member1_.zipcode as zipcode6_6_1_,
member1_.name as name7_6_1_,
delivery2_.created_at as created_2_3_2_,
delivery2_.modified_at as modified3_3_2_,
delivery2_.city as city4_3_2_,
delivery2_.street as street5_3_2_,
delivery2_.zipcode as zipcode6_3_2_,
delivery2_.status as status7_3_2_
from
orders order0_
inner join
member member1_
on order0_.member_id=member1_.member_id
inner join
delivery delivery2_
on order0_.delivery_id=delivery2_.delivery_id limit ?
limit ?
select
orderitems0_.order_id as order_id7_8_1_,
orderitems0_.order_item_id as order_it1_8_1_,
orderitems0_.order_item_id as order_it1_8_0_,
orderitems0_.created_at as created_2_8_0_,
orderitems0_.modified_at as modified3_8_0_,
orderitems0_.count as count4_8_0_,
orderitems0_.item_id as item_id6_8_0_,
orderitems0_.order_id as order_id7_8_0_,
orderitems0_.orderprice as orderpri5_8_0_
from
order_item orderitems0_
where
orderitems0_.order_id in (
?, ?
)
select
item0_.item_id as item_id2_4_0_,
item0_.created_at as created_3_4_0_,
item0_.modified_at as modified4_4_0_,
item0_.name as name5_4_0_,
item0_.price as price6_4_0_,
item0_.stockquantity as stockqua7_4_0_,
item0_1_.artist as artist1_0_0_,
item0_1_.etc as etc2_0_0_,
item0_2_.author as author1_1_0_,
item0_2_.isbn as isbn2_1_0_,
item0_3_.actor as actor1_7_0_,
item0_3_.director as director2_7_0_,
item0_.dtype as dtype1_4_0_
from
item item0_
left outer join
album item0_1_
on item0_.item_id=item0_1_.item_id
left outer join
book item0_2_
on item0_.item_id=item0_2_.item_id
left outer join
movie item0_3_
on item0_.item_id=item0_3_.item_id
where
item0_.item_id in (
?, ?, ?, ?
)
@~~ToOne
연관관계인 정보들 페치 조인으로 한번에 다 가져옴@~~ToMany
연관관계인 컬렉션 정보들은 지연 로딩으로 가져옴→ 만약 테이블 join 결과에 1측 테이블 중복 데이터가 너무 많을 경우, 페이징이 필요 없더라도 V3.1 방식이 더 좋을 수 있다.
@~~ToOne
관계는 페치 조인 해도 페이징에 영향을 주지 않는다.@~~ToOne
관계는 페치 조인으로 쿼리 수를 줄이고, 나머지는 hibernate.default_batch_fetch_size
로 최적화하자.