Entity를 DTO로 변환: 페치 조인
@RestController
@RequiredArgsConstructor
public class OrderApiController {
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
return orders.stream().map(OrderDto::new).collect(Collectors.toList());
}
@Data
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemDto> orderItems;
public OrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
orderItems = order.getOrderItems().stream()
.map(OrderItemDto::new)
.collect(Collectors.toList());
}
}
@Data
static class OrderItemDto {
private String itemName;
private int orderPrice;
private int count;
public OrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName();
orderPrice = orderItem.getItem().getPrice();
count = orderItem.getCount();
}
}
}
안에 있는 필드도 Entity를 그대로 노출하면 안된다.
OrderDto에는 OrderItem이 아니라 OrderItemDto 형태로 있어야 한다.
Address 같은 단순 값 타입은 변경될 일이 없으므로 바로 써도 상관없다.
지연 로딩 쿼리 횟수
order
1번
member, address, orderItem
order 결과 개수만큼
item
orderItem 결과 개수만큼
다만, 같은 Entity가 영속성 컨텍스트에 있다면 지연 로딩이더라도 SQL을 실행하지 않는다.
페치 조인 최적화
쿼리가 너무 많이 나가는 문제를 페치 조인으로 해결해보자.
@Repository
@RequiredArgsConstructor
public class OrderRepository {
public List<Order> findAllWithItem() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" +
" join fetch oi.item i", Order.class)
.getResultList();
}
}
[
{
"orderId": 4,
"name": "userA",
"orderDate": "2022-05-01T12:07:06.612872",
"orderStatus": "ORDER",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"orderItems": [
{
"itemName": "JPA1 BOOK",
"orderPrice": 10000,
"count": 1
},
{
"itemName": "JPA2 BOOK",
"orderPrice": 20000,
"count": 2
}
]
},
{
"orderId": 4,
"name": "userA",
"orderDate": "2022-05-01T12:07:06.612872",
"orderStatus": "ORDER",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"orderItems": [
{
"itemName": "JPA1 BOOK",
"orderPrice": 10000,
"count": 1
},
{
"itemName": "JPA2 BOOK",
"orderPrice": 20000,
"count": 2
}
]
},
{
"orderId": 11,
"name": "userB",
"orderDate": "2022-05-01T12:07:06.643625",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"orderItems": [
{
"itemName": "SPRING1 BOOK",
"orderPrice": 20000,
"count": 3
},
{
"itemName": "SPRING2 BOOK",
"orderPrice": 40000,
"count": 4
}
]
},
{
"orderId": 11,
"name": "userB",
"orderDate": "2022-05-01T12:07:06.643625",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"orderItems": [
{
"itemName": "SPRING1 BOOK",
"orderPrice": 20000,
"count": 3
},
{
"itemName": "SPRING2 BOOK",
"orderPrice": 40000,
"count": 4
}
]
}
]
문제는 order 결과값 2개, orderItem 4개를 조인하면 order가 4개로 뻥튀기 된다는 것이다.
select *
from orders o
join order_item oi on o.order_id = oi.order_id;

order와 order Item을 조인하면 중복된 결과가 나온다.
order는 2개지만 order_item에는 각 order_id에 해당하는 데이터가 2개씩 총 4개가 있기 때문이다.
즉, order_item 개수만큼 데이터가 뻥튀기 된다.

뻥튀기 된 데이터는 레퍼런스마저 똑같다.
JPA에서는 PK가 같으면 같은 참조값을 가지기 때문이다.
@Repository
@RequiredArgsConstructor
public class OrderRepository {
public List<Order> findAllWithItem() {
return em.createQuery(
"select distinct o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" +
" join fetch oi.item i", Order.class)
.getResultList();
}
}
[
{
"orderId": 4,
"name": "userA",
"orderDate": "2022-05-01T12:06:19.629212",
"orderStatus": "ORDER",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"orderItems": [
{
"itemName": "JPA1 BOOK",
"orderPrice": 10000,
"count": 1
},
{
"itemName": "JPA2 BOOK",
"orderPrice": 20000,
"count": 2
}
]
},
{
"orderId": 11,
"name": "userB",
"orderDate": "2022-05-01T12:06:19.664464",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"orderItems": [
{
"itemName": "SPRING1 BOOK",
"orderPrice": 20000,
"count": 3
},
{
"itemName": "SPRING2 BOOK",
"orderPrice": 40000,
"count": 4
}
]
}
]
컬렉션의 데이터 뻥튀기를 막기 위해 distinct로 중복을 거른다.
DB의 distinct
한 줄이 완전히 똑같아야 제거된다.
몇몇 상황에서는 중복 데이터의 모든 칼럼 데이터가 똑같지 않아 제거되지 않는다.
ex. order는 겹치지만 order_item 값은 겹치지 않아서 제거되지 않는다.
JPA의 distinct
SQL에 distinct를 추가해서 실제 distinct 쿼리가 나간다.
DB상에서는 distinct를 붙이나 안 붙이나 제거되지 않고 들어온다.
애플리케이션 상에서 다시 한 번 중복을 거른다.
조회 결과에 같은 Entity가 조회되면 즉, 레퍼런스가 같은 중복 데이터가 있으면 날린다.
페이징이 불가능하다는 단점이 있다.
페이징을 설정해도 limit, offset 쿼리가 나가지 않는다.
order가 중복 2개씩 총 4개가 있는데 페이징으로 order 1을 건너뛰어도 그 다음 페이지에 중복인 order 1이 다시 들어가서 이상해진다.
우리가 원하는 건 order 2개인데 order_item 기준으로 페이징 된다.
컬렉션 fetch join에서 페이징을 사용하면 모든 데이터를 DB에서 일단 읽어온 뒤, 메모리에서 페이징하면서 OOM이 발생할 수 있다.
컬렉션의 fetch join
컬렉션 페치 조인은 1개만 사용할 수 있다.
2개 이상의 컬렉션에 사용하면 안된다.
1대 다의 다가 되면서 다 * 다가 되므로 데이터가 완전히 뻥튀기 되면서 부정합하게 조회된다.
Last updated
Was this helpful?