Entity -> Dto 변환 시 Collection 도 잊지 말고 변환해야 한다
Collection을 dto 로 변환하여 리턴할 경우에도 엔티티에 대한 의존도를 끊어야 한다.
예시 코드를 보자.
- 흐름도
- Order 엔티티 정보
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
@Id
@GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // 지연로딩
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL) /* cascade 옵션을 써서 order 만 저장하면 orderitems 가 함께 저장 */
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
private LocalDateTime orderDate; // 주문시간
private OrderStatus status; // 주문 상태
...
}
- repository 에서 데이터 조회, entity로 반환
// repository
public List<OrderDto> orderV3() {
List<Order> orders = orderRepository.findAllWithItem();
return orders.stream().map( order -> new OrderDto(order))
.collect(toList());
}
- collection 내 데이터에 대하여 dto 로 변환
@Data
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemDto> orderItems;
public Long getOrderId() {
return orderId;
}
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(orderItem -> new OrderItemDto(orderItem))
.collect(toList());
}
}
위 코드에서 핵심인 부분은
orderItems = order.getOrderItems().stream()
.map(orderItem -> new OrderItemDto(orderItem))
.collect(toList());
이다. 단순하게 Order -> OrderDTO 로 처리하는 것이 아니라 내부에 있는 Collection 도 모두 Dto로 바꿔줘야 한다.
일대다 ( OneToMany )를 페치조인하는 경우 페이징은 문제가 발생한다
일대다 조인의 문제점은 출력되는 데이터의 결과가 증가하는 것이다. ( 1 + N 문제 발생 ) 따라서 데이터 결과 개수가 정확하게 측정되지 않는다.
그렇기 때문에 페이징이 불가능하다. 따라서 컬렉션에서 페치조인은 여러개 사용하기보다 하나만 사용하는 것이 권장된다.
현재 spring 3.x 에서 사용하는 hibernate 6 이 사용되면서 자동으로 중복제거를 해주긴 한다.
이 내용은 2024.02.17 - [개발/jpa] - JPQL의 개념과 간단한 사용 방법 ( 2 / 2 )에서 보다 자세하게 설명했다.
컬렉션을 조회할 때 페치조인과 성능 최적화 - 페이징을 위한 주의점
이 문제는 컬렉션을 페치조인할 때 주의해 아하는 내용이다.
페치조인은 즉시로딩이기 때문에 강제 Lazy초기화가 되어 한꺼번에 조회할 수 있어서 성능이 향상이점을 얻을 수 있다. xToOne 관계에서는 페치조인을 하더라도 페이징에 영향을 주지 않으므로 페치조인을 사용해서 쿼리 수를 줄이는 최적화를 진행하면 된다.
하지만 xToMany 에서는 출력 데이터의 증가 문제가 발생하기 때문에 hibernate.default_batch_fetch_size를 통해 최적화를 진행해야 한다.
hibernate.default_batch_fetch_size를 사용하면 지정한 개수만큼 in을 걸어서 한 번에 모든 데이터를 가져올 수 있다. 예를들어, 아이템에 lazy 초기화가 되어 있어 하나하나 조회하던 내용에 대해 in으로 묶어서 한 번에 가져올 수 있다.
default_batch_fetch_size의 적당한 사이즈는 100~ 1000 사이가 권장된다.
JPQL에서 new 생성자를 통해 dto 형식으로 반환할 때 컬렉션 항목은 포함될 수 없다
em.createQuery("select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, o.name, o.orderDate)
from Order o" +
" join o.member m" +
" join o.delivery d", OrderItemQueryDto.class)
.getResultList();
Order 에는 OrderItems라는 Collection 항목이 있지만, 그 필드는 제외하고 가져와야 한다. new
public List<OrderQueryDto> findOrderQueryDtos() {
List<OrderQueryDto> result = findOrders();
// 직접 컬렉션을 다시 넣어준다
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
public List<OrderQueryDto> findOrders() {
return em.createQuery("select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, o.name, o.orderDate)
from Order o" +
" join o.member m" +
" join o.delivery d", OrderQueryDto.class)
.getResultList();
}
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi" +
" join oi.item i" +
" where oi.order.id = :orderId", OrderItemQueryDto.class)
.setParameter("orderId", orderId)
.getResultList();
}
생성자를 통해 바로 가져올 수 없기 때문에 아래 코드처럼 직접 컬렉션을 넣어주는 작업이 필요하다.
그런데 이렇게 하면 문제가 발생한다.
Orders에 포함된 OrderItems 개수만큼 N 번의 쿼리 조회가 발생하게 된다 따라서 아래처럼 합칠 수 있다.
public List<OrderQueryDto> findAllByDto_optimization() {
// 주문을 모두 가져옴
List<OrderQueryDto> result = findOrders();
// orderId 에 대한 리스트를 생성
List<Long> orderIds = result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
// orderIds 를 in 을 통해서 한번에 가져오기
List<OrderItemQueryDto> orderItems = em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi" +
" join oi.item i" +
" where oi.order.id in :orderIds", OrderItemQueryDto.class)
.setParameter("orderIds", orderIds)
.getResultList();
// map 으로 바꾸는 최적화 진행 ( 쿼리는 한번 날리고 메모리에서 값을 세팅하는 방법 )
Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
.collect((Collectors.groupingBy(OrderItemQueryDto -> OrderItemQueryDto.getOrderId())));
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
위 코드를 실행하면 Order를 조회할 때 한번, Orderitems를 in으로 묶어서 가져올 때 한번 해서 총 두 번으로 쿼리 조회가 줄게 된다.
select
o1_0.order_id,m1_0.name,o1_0.order_date,o1_0.status,d1_0.city,d1_0.street,d1_0.zipcode
from orders o1_0
join member m1_0
on m1_0.member_id=o1_0.member_id
join delivery d1_0
on d1_0.delivery_id=o1_0.delivery_id.
select
oi1_0.order_id,i1_0.name,oi1_0.order_price,oi1_0.count
from order_item oi1_0
join item i1_0
on i1_0.item_id=oi1_0.item_id
where oi1_0.order_id in (1,2);
만약 페이징이 필요 없는 경우라면 한꺼번에 조회해서 가져오는 방법도 있다.
public List<OrderFlatDto> findAllByDto_flat() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderFlatDto(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count)" +
" from Order o" +
" join o.member m" +
" join o.delivery d" +
" join o.orderItems oi" +
" join oi.item i", OrderFlatDto.class )
.getResultList();
}
select o1_0.order_id,m1_0.name,o1_0.order_date,o1_0.status,d1_0.city,d1_0.street,d1_0.zipcode,i1_0.name,oi1_0.order_price,oi1_0.count
from orders o1_0
join member m1_0
on m1_0.member_id=o1_0.member_id
join delivery d1_0
on d1_0.delivery_id=o1_0.delivery_id
join order_item oi1_0
on o1_0.order_id=oi1_0.order_id
join item i1_0
on i1_0.item_id=oi1_0.item_id;
이렇게 가져올 경우 OrderQueryDto로 반환하기 위해 서버단에서 다음과 같이 수정하는 작업이 필요하다
// 반환을 OrderQueryDto 로 하기 위해 변환
return flats.stream()
.collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
mapping(o -> new OrderItemQueryDto(o.getOrderId(), o.getItemName(), o.getOrderPrice(), o.getCount()), toList())
)).entrySet().stream()
.map(e -> new OrderQueryDto(e.getKey().getOrderId(), e.getKey().getName(), e.getKey().getOrderDate(), e.getKey().getOrderStatus(), e.getKey().getAddress(), e.getValue()))
.collect(toList());
- OrderQueryDto
@Data
@EqualsAndHashCode(of = "orderId")
public class OrderQueryDto {
public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address, List<OrderItemQueryDto> orderItems) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
this.orderItems = orderItems;
}
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemQueryDto> orderItems;
public OrderQueryDto(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;
}
}
@EqualsAndHashCode(of = "orderId")가 있어야 orderId를 기준으로 묶인다. ( equals 랑 hashcode는 자주 나오기 때문에...@EqualsAndHashCode 관련해서는 곧 게시글을 작성해야겠다. )
'Dev > JPA' 카테고리의 다른 글
OSIV 를 통한 성능 최적화 ( Connection 관리 관점 ) (1) | 2024.03.15 |
---|---|
org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [1] did not match expected type [java.lang.String (n/a)] 오류 해결하기 (0) | 2024.02.25 |
JPQL 의 개념과 간단한 사용 방법 ( 2 / 2 ) (1) | 2024.02.17 |
JPQL 의 개념과 간단한 사용 방법 ( 1 / 2 ) (1) | 2024.02.17 |
JPA 데이터 타입 분류 (2) | 2024.02.15 |