JPA
실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 강의 정리본
JPA와 컬렉션에 대한 이해를 사용해보자
1.Entity로 조회
1.1 Lazy로딩 이용
Repository
/**
* JPQL 동적으로 문자열 구성
* @param orderSearch
* @return List<Order>
*/
public List<Order> findAllByString(OrderSearch orderSearch) {
// language=JPQL
String jpql = "select o From Order o join o.member m";
boolean isFirstCondition = true;
// 주문 상태 검색
if (orderSearch.getOrderStatus() != null) {
if (isFirstCondition) {
jpql += " where";
isFirstCondition = false;
} else {
jpql += " and";
}
jpql += " o.status = :status";
}
// 회원 이름 검색
if (StringUtils.hasText(orderSearch.getMemberName())) {
if (isFirstCondition) {
jpql += " where";
isFirstCondition = false;
} else {
jpql += " and";
}
jpql += " m.name like :name";
}
TypedQuery<Order> query = em.createQuery(jpql, Order.class).setMaxResults(1000); // 최대 1000건
if (orderSearch.getOrderStatus() != null) {
query = query.setParameter("status", orderSearch.getOrderStatus());
}
if (StringUtils.hasText(orderSearch.getMemberName())) {
query = query.setParameter("name", orderSearch.getMemberName());
}
return query.getResultList();
}
Controller
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
// 지연로딩이기때문에 order.getMember => null, order.getOeOrdPrd => null
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
DTO
@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(); // lazy 강제 초기화
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); // lazy 강제 초기화
orderItems = order.getOrderItems().stream()
.map(orderItem -> new OrderItemDto(orderItem))
.collect(toList());
}
}
@Data
static class OrderItemDto {
private String itemName;//상품 명
private int orderPrice; //주문 가격
private int count; //주문 수량
public OrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName(); // lazy 강제 초기화
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
- dao에서 가져온 데이터
- Order1 :
ordNo: 1, ordDt: 20250224
- Order2 :
ordNo: 2, ordDt: 20250225
- Order1 :
- 지연 로딩으로 너무 많은 SQL 실행
- SQL 실행 수
- order 1번
- member , address N번(order 조회 수 만큼)
- orderItem N번(order 조회 수 만큼)
- item N번(orderItem 조회 수 만큼)
- 여기서 조회수 의미와 1+N
- 실제 시퀸스 조회수
- 만약
member
테이블과team
테이블이 있다 가정..
DB
|ㅤmbrIdㅤㅤ|ㅤㅤteamIdㅤ|
|ㅤㅤㅤ1ㅤㅤ|ㅤㅤㅤAㅤㅤㅤ|
|ㅤㅤㅤ2ㅤㅤ|ㅤㅤㅤAㅤㅤㅤ|
|ㅤㅤㅤ3ㅤㅤ|ㅤㅤㅤBㅤㅤㅤ|
select m member m join team t
를 실행했을 때.. 1번 쿼리실행됨members.stream().forEach(member -> { System.out.println(member.getTeamNm()); });
- 위 루프를 돌때마다 일어나는 일
- member 1 조회될때 =>
select t from team t where teamId = A
실행 - member 2 조회할때 => 이미 위에서 조회했으므로 영속성Context에서 조회
- member 3 조회할때 =>
select t from team t where teamId = B
실행 - 최대 1(
member
조회) + N(team
조회)이 실행됨
- 지연로딩때문에 너무 많은 쿼리가 실행되는 문제가 발생
1.2 fetch join 적용
Repository
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();
}
distnct
를 안쓴다면?- 아래처럼 나온다
DB조회결과
————————————————————————-————————————————————
|ㅤordNoㅤ|ㅤㅤordDtㅤㅤㅤ|memberNmㅤ|ㅤprdSeqㅤ|ㅤㅤprdIdㅤㅤㅤㅤ|ㅤㅤprdNmㅤㅤ|
|ㅤㅤㅤ1ㅤㅤ|ㅤ20250224ㅤ|ㅤㅤ김찬영ㅤㅤ|ㅤㅤㅤ1ㅤㅤㅤ|ㅤㅤㅤ1542ㅤㅤㅤ|ㅤㅤㅤ연필ㅤㅤ|
|ㅤㅤㅤ1ㅤㅤ|ㅤ20250224ㅤ|ㅤㅤ김찬영ㅤㅤ|ㅤㅤㅤ2ㅤㅤㅤ|ㅤㅤㅤ1252ㅤㅤㅤ|ㅤㅤㅤ지우개ㅤ|
|ㅤㅤㅤ2ㅤㅤ|ㅤ20250225ㅤ|ㅤㅤ김찬영ㅤㅤ|ㅤㅤㅤ1ㅤㅤㅤ|ㅤㅤㅤ1542ㅤㅤㅤ|ㅤㅤㅤ연필ㅤㅤ|
|ㅤㅤㅤ2ㅤㅤ|ㅤ20250225ㅤ|ㅤㅤ김찬영ㅤㅤ|ㅤㅤㅤ2ㅤㅤㅤ|ㅤㅤㅤ849 ㅤㅤㅤ|ㅤㅤㅤ분필ㅤㅤ|
—————————————————————————————————————————————
-
why? fetch join을 하기때문에 즉각적으로 한번에 필요한 데이터를 가져오기때문에 뻥튀기가 된다
-
distinct
를 쓴다면?- 일단 DB단에서 관련된 값들을 전부 조회해옴(물론 이때도 DB에
distinct
를 사용함) Order
객체와 관련된 엔티티(OeOrdPrd
,Member
)들을 한꺼번에 객체 주입Order
객체 주소값을 기준으로 중복이 있으면 제거됨- 완벽한것같지만.. 페이징(ex 1번부터 100번까지 가져와)같은 처리가 불가능 => 앱단에서 페이징을 하기때문!
- 일단 DB단에서 관련된 값들을 전부 조회해옴(물론 이때도 DB에
1:N:N과 같은 경우는 안된다!
1.3 페치조인 보안
Repository
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
ToOne관계만 fetch Join하고 나머지는 hibernate.default_batch_fetch_size
를 통하여 관련된 OeOrdPrd
를 여러개 갖고온다
select ... OeOrdPrd o where ordNo in (1511, 1512, ..., 1610);
2. DTO로 조회(직접 조회)
2.1 1 + N (컬렉션)
DTO
@Data
@EqualsAndHashCode(of = "orderId")
public class OrderQueryDto {
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;
}
}
@Data
public class OrderItemQueryDto {
@JsonIgnore
private Long orderId; //주문번호
private String itemName;//상품 명
private int orderPrice; //주문 가격
private int count; //주문 수량
public OrderItemQueryDto(Long orderId, String itemName, int orderPrice, int
count) {
this.orderId = orderId;
this.itemName = itemName;
this.orderPrice = orderPrice;
this.count = count;
}
}
Repository
/**
* 컬렉션은 별도로 조회
* Query: 루트 1번, 컬렉션 N 번
* 단건 조회에서 많이 사용하는 방식
*/
public List<OrderQueryDto> findOrderQueryDtos() {
//루트 조회(toOne 코드를 모두 한번에 조회)
List<OrderQueryDto> result = findOrders();
//루프를 돌면서 컬렉션 추가(추가 쿼리 실행)
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
/**
* 1:N 관계(컬렉션)를 제외한 나머지를 한번에 조회
*/
private List<OrderQueryDto> findOrders() {
return em.createQuery(
"select new
jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name, o.orderDate,
o.status, d.address)" +
" from Order o" +
" join o.member m" +
" join o.delivery d", OrderQueryDto.class)
.getResultList();
}
/**
* 1:N 관계인 orderItems 조회
*/
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();
}
- 쿼리 실행 횟수: 루트 1번, 컬렉션 N 번 실행
- ToOne(N:1, 1:1) 관계들을 먼저 조회하고(
findOrders()
), ToMany(1:N) 관계는 각각 별도로 처리(findOrderItems()
)한다.- ToOne 관계는 조인해도 데이터 row 수가 증가하지 않는다.
- ToMany(1:N) 관계는 조인하면 row 수가 증가한다.
- row 수가 증가하지 않는 ToOne 관계는 조인으로 최적화 하기 쉬우므로 한번에 조회하고, ToMany 관계는 최적화 하기 어려우므로
findOrderItems()
같은 별도의 메서드로 조회한다.
fetch join이 아닌데도 불구하고 한번에 되는 이유?
여기서는 엔티티를 조회하는 것이 아니라 원하는 데이터를 모두 조인해서 한번에 필요한 데이터만 조회하는 방식을 사용했습니다. 이러한 조회 방식을 DTO로 조회하는 방식이라고 일반적으로 이야기합니다.
이렇게 DTO로 조회하게 되면 엔티티가 아닙니다. 따라서 지연로딩, Fetch join등을 사용할 수 없습니다.
이렇게 DTO로 조회하려면 SQL의 JOIN 문을 사용해서 처음부터 원하는 데이터를 모두 선택해서 조회해야 합니다.
2.2 1:N의 쿼리를 IN절로 조회
Repository
/**
* 최적화
* Query: 루트 1번, 컬렉션 1번
* 데이터를 한꺼번에 처리할 때 많이 사용하는 방식
*
*/
public List<OrderQueryDto> findAllByDto_optimization() {
//루트 조회(toOne 코드를 모두 한번에 조회)
List<OrderQueryDto> result = findOrders();
//orderItem 컬렉션을 MAP 한방에 조회
Map<Long, List<OrderItemQueryDto>> orderItemMap =
findOrderItemMap(toOrderIds(result));
//루프를 돌면서 컬렉션 추가(추가 쿼리 실행X)
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
private List<Long> toOrderIds(List<OrderQueryDto> result) {
return result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
}
private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds)
{
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();
return orderItems.stream()
.collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
}
- 쿼리 실행 횟수: 루트 1번, 컬렉션 1번
- ToOne 관계들을 먼저 조회하고, 여기서 얻은 식별자
orderId
로 ToMany 관계인OrderItem
을 한꺼번에 조회
2.3 한방쿼리
- 모두 한꺼번에 한방쿼리로 조회하고 자바단에서 맞춰준다.
Repository
@Data
public class OrderFlatDto {
private Long orderId;
private String name;
private LocalDateTime orderDate; //주문시간
private Address address;
private OrderStatus orderStatus;
private String itemName;//상품 명
private int orderPrice; //주문 가격
private int count; //주문 수량
public OrderFlatDto(Long orderId, String name, LocalDateTime orderDate,
OrderStatus orderStatus, Address address, String itemName, int orderPrice, int
count) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
this.itemName = itemName;
this.orderPrice = orderPrice;
this.count = count;
}
}
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();
}
OrderQueryDto
에 List<OrderItemQueryDto>
를 추가로 인자 받는 생성자 추가
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;
}
Controller
@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ordersV6() {
List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();
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());
}
댓글 쓰기