0. 개요
- 이번 포스팅에서는 DTO를 직접 사용하여 컬렉션을 조회하는 방식에 대해서 알아보자.
1. DTO 직접 조회 준비
a) DTO 직접 조회의 의미
- 단일 조회 시 DTO를 직접 사용한 조회 방법과 동일하게 컬렉션도 DTO를 사용하여 직접 조회할 수 있다.
- DTO 직접 조회 방식의 핵심은 Entity를 전혀 사용하지 않고 오직 DTO만을 이용하여 데이터를 받아오는 것이다.
- 즉, Entity를 조회하고 그 데이터를 이용하여 DTO를 생성 및 반환하는 작업이다.
- 언뜻 보면 DTO 변환 방식과 특별한 차이점을 못 느낄 수도 있다.
- 그러나 DTO 직접 조회 방식은 API 메서드에서 오직 DTO만을 사용한다는 것이 특징이다.
b) 준비 작업 확인
- 우선 DTO 변환을 이용한 조회 메서드를 기반으로 DTO 직접 조회에 필요한 것들을 확인해보자.
- DTO 변환 조회 메서드의 코드는 다음과 같다.
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o)).collect(Collectors.toList());
return result;
}
- 위의 코드를 하나씩 분석해보자.
- 우선, orderRepository의 메서드를 사용하여 Order Entity를 찾아왔다.
- stream() 메서드로 찾아온 Order 객체를 OrderDTO로 변환하였다.
- 이 과정에서 필요한 2가지를 알 수 있다.
→ DTO를 반환하는 Repository가 필요하다.
→ Repository에서 반환할 DTO가 필요하다.
- 그렇다면 이 2가지를 준비하면서 또 무엇이 필요한지 차근차근 만들어가 보자.
2. DTO 반환 Repository 생성
a) DTO 생성
- 우선 Repository를 만들기 전에, Repository에서 반환할 DTO가 필요하다.
- 그렇다면 기존에 사용하던 OrderDTO의 형태를 확인해보자,
- OrderDTO는 다음과 같이 구성되었다.
@Data
public class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemDto> orderItems;
// 생성자 생략
}
- OrderDto의 필드를 살펴보자.
- 내부에 컬렉션인 orderItems가 존재한다. 컬렉션 필드의 자료형 또한 DTO로 만들어 줘야 한다.
- 다만, 여기서 주의할 점은 DTO를 이용해 직접 조회할 때 컬렉션 필드를 함께 사용해서는 안된다는 점이다.
- 컬렉션 필드를 조회에 사용하면 Join이 필수다.
- 그러나 일대다(OneToMany) 관계를 Join 하면 데이터 뻥튀기가 발생한다.
- 그러므로 다음과 같이 DTO를 구성할 수 있다.
@Data
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;
}
}
- 위의 DTO의 핵심은 생성자에 있다.
- 생성자를 살펴보면 컬렉션 필드인 orderItems가 생략되어 있다.
- 데이터 뻥튀기 문제를 해결하기 위해, 후처리 과정으로 직접 주입하는 방식을 사용할 예정이다.
b) 컬렉션 필드를 위한 DTO 생성
- 컬렉션 필드의 자료형은 다른 Entity를 참조한다.
- 원래는 OrderItems를 참조하지만, 이는 Entity를 노출하는 방식이므로 DTO를 만들어준다.
@Data
public class OrderItemQueryDto {
@JsonIgnore // 화면에 데이터를 뿌리기 위한 DTO 이므로 orderId 값은 중요하지 않음.
private Long orderId;
private String itemName;
private int orderPrice;
private int count;
// 생성자 생략
}
c) DTO를 이용한 조회
- OrderQueryDto를 이용하여 DB로부터 조회 결과를 받아온다.
- 이를 위해서 다음과 같은 JPQL 메서드를 사용한다.
// Repository에 작성
private List<OrderQueryDto> findOrders() {
// OrderQueryDto의 생성자에서 OrderItems 라는 컬렉션 필드는 제외되었다.
// 그 이유는 컬렉션이기 때문이다.
// DB의 관점에서는 한줄의 데이터가 삽입되어야 한다.
// 그러나 컬렉션이 포함되는 순간, 일대다 관계이므로 데이터의 수가 뻥튀기 된다.
// 그러므로 쿼리를 작성하는 부분에서는 데이터 뻥튀기를 고려하여 다음과 같이 작성한다.
return em.createQuery(
"select new jpabook.jpabook.dto.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.adderss) " +
"from Order o " +
"join o.member m " +
"join o.delivery d", OrderQueryDto.class).getResultList();
}
- 위의 JPQL을 이용하여 DB로부터 직접 OrderQueryDto에 데이터를 받아온다.
- 그러나 아직 끝이 아니다. 컬렉션 필드인 OrderItems의 값이 빠져있기 때문이다.
d) 컬렉션 필드 채워 넣기
- 다음과 같은 방식으로 OrderQueryDto 객체에 OrderItems 값을 채워 넣는다.
- 이를 위해서 DB로부터 OrderItem의 값을 조회해야 하는데, 이를 findOrderItems() 메서드에서 수행한다.
// Repository에 작성
public List<OrderQueryDto> findOrderQueryDtos() {
// result는 컬렉션 필드(orderItems)가 비어있는 상태
List<OrderQueryDto> result = findOrders();
// 컬렉션 필드를 직접 채우는 과정
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
- findOrderItems() 메서드 내부에서는 다음과 같은 JPQL을 이용하여 OrderItem의 값을 받아온다.
- 해당 JPQL에서 반환된 조회 결과는 OrderItemQueryDto에 저장된다.
// Repository에 작성
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
// 컬렉션 필드의 값을 조회하는 JPQL
return em.createQuery(
"select new jpabook.jpabook.dto.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();
}
- 위의 findOrderQueryDto() 메서드는 다음과 같은 과정을 거친다.
→ OrderQueryDto를 사용하여 DB에서 조회된 데이터를 받는다. (이때 컬렉션 필드는 조회 대상에서 제외된다.)
→ OrderQueryDto 객체에는 컬렉션 필드인 OrderItems 값이 비어있는 상황이다.
→ 반복문으로 OrderQueryDto 객체를 탐색하며 findOrderItems() 메서드를 호출한다.
→ findOrderItems() 메서드는 OrderQueryDto 객체의 Id 값을 이용하여 DB로부터 OrderItem의 값을 조회한다.
→ 조회된 OrderItem의 값은 OrderItemQueryDto 객체에 담기며, 이를 orderItems라고 명명하였다.
→ 마지막으로 orderItems를 OrderItemQueryDto에 할당한다.
e) Repository 전체 코드
- 여기까지 DTO 직접 조회를 구현하였다.
- Repository와 이를 호출하는 Controller의 전체적인 코드는 다음과 같이 구성된다.
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
public List<OrderQueryDto> findOrderQueryDtos() {
List<OrderQueryDto> result = findOrders();
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
private List<OrderQueryDto> findOrders() {
return em.createQuery(
"select new jpabook.jpabook.dto.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.adderss) " +
"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.jpabook.dto.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();
}
}
- Controller에서는 다음과 같이 OrderQueryRepository를 이용한다.
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderQueryRepository orderQueryRepository;
@GetMapping("/api/v4/orders")
public List<OrderQueryDto> orderV4 () {
return orderQueryRepository.findOrderQueryDtos();
}
}
3. N + 1 문제
a) 쿼리 발행 지점 확인하기
- 다음 코드를 보자.
// Repository에 작성
public List<OrderQueryDto> findOrderQueryDtos() {
// result는 컬렉션 필드(orderItems)가 비어있는 상태
List<OrderQueryDto> result = findOrders();
// 컬렉션 필드를 직접 채우는 과정
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
- 위의 코드는 DB로부터 받아온 OrderQueryDto 객체에 비어있는 OrderItem 객체를 채워 넣는 과정을 거친다.
- findOrderQueryDtos() 메서드를 실행하면 총 몇 번의 쿼리가 발행될지 생각해보자.
- 우선 findOrders() 메서드에서 쿼리가 1회 발행된다.
- 그리고 findOrderItems() 메서드에서 쿼리가 N번 발행된다.
- findOrderItems() 메서드는 반복문이 돌아가는 만큼 실행되기 때문이다.
- 즉, 결과적으로 N + 1 문제가 발생한다.
b) N + 1 문제의 원인
- N + 1 문제의 원인은 JPQL의 WHERE 절을 살펴보면 쉽게 찾을 수 있다.
- 대부분 N + 1 문제는 WHERE 절에서 비교 또는 검색 조건이 하나씩 처리되기 때문에 발생한다.
- 다음 예시를 보면 쉽게 이해할 것이다.
- 위 사진을 살펴보자.
- 빨간 줄이 그어진 WHERE 절을 보면 비교대상(= orderId)이 하나뿐이다.
- 즉, 하나의 쿼리마다 비교 대상이 하나씩이므로 다수(= 컬렉션 필드)를 조회하는 경우에는
위의 쿼리를 반복적으로 실행할 수밖에 없다.
- 이처럼 비교 또는 검색 조건이 하나만 사용되는 경우, 컬렉션의 개수만큼 쿼리가 발행되고 이는 N + 1 문제가 된다.
c) N + 1 문제의 해결법(IN 키워드)
- 이 문제의 해결책은 비교 또는 검색 조건을 여러 개 담을 수 있는 쿼리를 작성하는 것이다.
- 즉, IN 키워드를 사용한다.
- 다음 쿼리처럼 말이다.
public List<OrderQueryDto> findAllByDto() {
List<OrderQueryDto> result = findOrders();
List<Long> orderIds = result.stream().map(o -> o.getOrderId()).collect(Collectors.toList());
List<OrderItemQueryDto> orderItems = em.createQuery(
"select new jpabook.jpabook.dto.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();
// orderItems를 Map으로 변환 (사용하기 쉽게하기 위함)
Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
.collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId()));
// result의 orderItems 필드를 채우는 과정
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
- 위의 예시에서 JPQL 부분을 살펴보자.
- in 키워드를 사용하고 ordersIds라는 매개변수를 받는다.
- ordersIds는 Long 타입의 List 자료구조로, 다수의 Id 값을 가지고 있다.
- 이처럼 IN 키워드를 사용하여 N + 1 문제를 해결할 수 있다.
d) 총 몇 번의 쿼리가 발행될까?
- 위의 findAllByDto() 메서드는 총 2번의 쿼리를 발행한다.
- findOrders()에서 1번, orderItems를 찾아오는 JPQL에서 1번이 발행된다.
- 이마저도 단 한 번의 쿼리 발행으로 해결할 수 있는 방법은 없을까?
4. DTO 직접 조회를 한방 쿼리로
a) 2번의 쿼리가 발생한 이유
- 위에서 작성한 findAllByDto() 메서드는 총 2번의 쿼리를 발행한다.
- 이 2번의 쿼리 발행 시점은 다음과 같다.
→ OrderQueryDto로 Order 테이블을 조회할 때 (컬렉션 필드인 orderItems는 비어있는 상태)
→ 컬렉션 필드(orderItems)의 값을 OrderItem 테이블에서 조회할 때
- 당연하지만 2번의 JPQL이 사용되기 때문에 쿼리가 2번 발행되는 것이다.
- 즉, 컬렉션 필드(orderItems)의 값을 채워 넣는 과정에서 2개의 JPQL이 사용되기 때문이다.
- 그렇다면 이 과정을 하나의 JPQL로 처리하면 1번의 쿼리 발행으로 해결할 수 있지 않을까?
b) 1단계 - DTO 생성
- 컬렉션 필드의 값을 채워 넣는 과정을 없애기 위해서 새로운 DTO가 필요하다.
- 해당 DTO는 다음과 같이 구성한다.
@Data
public class OrderFlatDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
// 컬렉션 필드 대신 컬렉션 필드의 속성으로 구성
private String itemName;
private int orderPrice;
private int count;
// 생성자 생략
}
- 위의 코드를 살펴보자.
- 컬렉션 필드를 사용하지 않고, 컬렉션 필드의 속성을 개별 필드로 생성하였다.
- 이와 같은 형태로 구성하는 이유는 일대다 JOIN으로 발생하는 중복 데이터를 모두 받아오기 위해서다.
c) 2단계 - JPQL 작성
- 위의 DTO를 이용하여 다음과 같이 JPQL 메서드를 Repository에 작성한다.
public List<OrderFlatDto> findAllByDtoFlat() {
return em.createQuery(
"select new jpabook.jpabook.dto.OrderFlatDto(o.id, m.name, o.orderDate, o.status, " +
"d.adderss, 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();
}
- 위의 JPQL에서 알 수 있듯이, o.orderItems를 JOIN 하였다.
- 일대다 JOIN을 하였으므로 데이터 중복(= 데이터 뻥튀기)이 발생할 수밖에 없는 상황이다.
- 위의 JPQL을 실행해보면 다음과 같은 결과를 반환한다.
- 위의 결과 데이터는 PK(= id) 값이 동일한 객체가 2개씩 있으므로, 영속성 Context의 측면에서 중복 객체다.
- 즉, 해당 JPQL은 중복 데이터를 반환하는 것이다. (이는 일대다 JOIN으로 필수 불가결하다.)
- 그러므로 동일한 PK(= id) 값을 가진 객체끼리 묶어주는 작업을 수행해야 한다.
d) 3 단계 - 중복 객체 grouping
- 중복 객체를 묶어줄 뿐만 아니라, 기존의 컬렉션 필드(= orderItems)를 되돌려놔야 한다.
- 그러므로 다음과 같은 과정을 필요로 한다.
→ 주문 정보를 이용하여 OrderQueryDto를 만든다. (컬렉션 필드의 값은 비어있는 상태)
→ 기존의 컬렉션 필드의 속성을 이용하여 OrderItemQueryDto로 만든다.
→ 비어있는 OrderQueryDto의 컬렉션 필드에 OrderItemQueryDto를 넣어준다.
- 무슨 말인지 이해가 잘 가지 않을 것이다.
- 다음 후처리 과정의 코드를 보면서 이해해보자.
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
private final OrderQueryRepository orderQueryRepository;
@GetMapping("/api/v6/orders")
public List<OrderQueryDto> orderV6 () {
List<OrderFlatDto> flats = orderQueryRepository.findAllByDtoFlat();
return flats.stream()
// stream()을 이용하여 OrderFlatDto의 일부를 OrderQueryDto로 변환
.collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(),
o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
// stream()을 이용하여 OrderFlatDto의 일부를 OrderItemQueryDto로 변환
mapping(o -> new OrderItemQueryDto(o.getOrderId(), o.getItemName(),
o.getOrderPrice(), o.getCount()), toList())
)).entrySet().stream()
// 최종적으로 OrderQueryDto와 OrderItemQueryDto를 이용하여 OrderQueryDto 생성
.map(e -> new OrderQueryDto(e.getKey().getOrderId(),
e.getKey().getName(), e.getKey().getOrderDate(),
e.getKey().getOrderStatus(), e.getKey().getAddress(), e.getValue()))
.collect(toList());
}
}
- 위의 코드에서 return문을 살펴보자.
- OrderFlatDto는 컬렉션 필드 대신 컬렉션 필드의 속성으로 구성되어있다.
- findAllByDtoFlat() 메서드에서 OrderFlatDto 객체를 반환하면, 이를 이용하여 후처리 작업을 수행한다.
- 우선 OrderFlatDto의 일부 데이터를 이용하여 OrderQueryDto를 생성한다.
- 그리고 기존의 컬렉션 필드를 대신했던 컬렉션 필드의 속성 값을 이용하여 OrderItemQueryDto를 생성한다.
- 비어있는 OrderQueryDto의 컬렉션 필드를 OrderItemQueryDto로 채워 넣는다.
- 위의 코드의 핵심은 groupingBy()를 사용한 것이다.
- DB의 입장에서는 중복 데이터가 반환되는 것은 동일하다.
- 하지만, JPA는 groupingBy()를 통해 영속성 Context 상에서 동일한 객체의 값을 하나로 묶는다.
- 이는 다음과 같은 결과를 JSON으로 반환한다.
[
{
"orderId": 11,
"name": "userB",
"orderDate": "2022-06-04T16:04:57.769566",
"orderStatus": "ORDER",
"address": {
"city": "Busan",
"street": "Haeundae",
"zipcode": "22222"
},
"orderItems": [
{
"itemName": "Spring1 Book",
"orderPrice": 20000,
"count": 3
},
{
"itemName": "Spring2 Book",
"orderPrice": 40000,
"count": 4
}
]
},
{
"orderId": 4,
"name": "userA",
"orderDate": "2022-06-04T16:04:57.65529",
"orderStatus": "ORDER",
"address": {
"city": "Seoul",
"street": "Gangnam",
"zipcode": "11111"
},
"orderItems": [
{
"itemName": "JPA1 Book",
"orderPrice": 10000,
"count": 1
},
{
"itemName": "JPA2 Book",
"orderPrice": 20000,
"count": 2
}
]
}
]
- 이처럼 동일한 객체를 하나로 묶어주고, 동일 객체에 한해서 다른 데이터는 배열로 묶어서 표현한다.
e) groupingBy()의 기준 설정 - @EqualsAndHashCode
- groupingBy() 메서드는 SQL의 Group By와 동일한 기능을 수행한다.
- 다만, 객체 중점적이라는 차이만 있을 뿐이다.
- 그렇다면 groupingBy()는 어떤 값을 기준으로 객체를 묶는 것일까?
- groupingBy()가 적용되는 대상을 살펴보면 OrderQueryDto 객체인 것을 확인할 수 있다.
- 그러므로 groupingBy()의 기준은 OrderQueryDto 클래스에 다음과 같이 설정해준다.
@Data
@EqualsAndHashCode(of = "orderId") // v6 메서드의 groupingBy 에서 사용하는 묶음 기준
public class OrderQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemQueryDto> orderItems;
// 생성자 생략
}
- 위의 코드를 보면 @EqualsAndHashCode라는 어노테이션이 사용되었다.
- 그리고 해당 어노테이션의 매개변수로 "orderId"가 들어있는 것을 확인할 수 있다.
- 이는 OrderQueryDto 객체 간의 비교를 수행할 때, orderId 필드의 값을 기준으로 비교를 수행한다는 의미다.
- 즉, orderId 값이 동일한 객체끼리 groupingBy()를 통해 하나의 객체로 묶이는 것이다.
5. DTO 직접 조회 + 한방 쿼리 장단점
a) 장점
- 쿼리 발행 회수가 1회다.
- 데이터가 많지 않으면 작업 수행 속도가 빠르다.
b) 단점
- 일대다 JOIN으로 인하여 조회 결과에 중복 데이터가 발생된다.
- 그러므로 Order를 기준으로 페이징이 불가능하다. (= 데이터 뻥튀기, 중복 데이터 발생)
- 애플리케이션 측면세어 보면 후처리 작업이 크다.
6. 전체 코드 구성
- Controller
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderQueryRepository orderQueryRepository;
@GetMapping("/api/v6/orders")
public List<OrderQueryDto> orderV6 () {
List<OrderFlatDto> flats = orderQueryRepository.findAllByDtoFlat();
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());
}
}
- OrderFlatDto
@Data
public class OrderFlatDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
// 컬렉션 필드 대신 컬렉션 필드의 속성으로 구성
private String itemName;
private int orderPrice;
private int count;
// 생성자 생략
}
- OrderQueryDto
@Data
@EqualsAndHashCode(of = "orderId") // v6 메서드의 groupingBy 에서 사용하는 묶음 기준
public class OrderQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemQueryDto> orderItems;
// 생성자 생략
}
- OrderItemQueryDto
@Data
public class OrderItemQueryDto {
@JsonIgnore
private Long orderId;
private String itemName;
private int orderPrice;
private int count;
}
- OrderQueryRepository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
public List<OrderFlatDto> findAllByDtoFlat() {
return em.createQuery(
"select new jpabook.jpabook.dto.OrderFlatDto(o.id, m.name, o.orderDate, o.status, " +
"d.adderss, 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();
}
}
- 여기까지 DTO 직접 조회 방식으로 컬렉션을 조회하는 방법에 대해서 알아보았다.
'Back-end > JPA 개념' 카테고리의 다른 글
Spring Data JPA란? (0) | 2022.06.07 |
---|---|
OSIV(Open Session In View) (0) | 2022.06.07 |
API 조회 성능 최적화 - 컬렉션(Fetch Join + 페이징 + Batch Size) (0) | 2022.06.03 |
API 조회 성능 최적화 - 컬렉션(Fetch Join, distinct) (0) | 2022.06.02 |
API 조회 성능 최적화 - 컬렉션(DTO 변환) (0) | 2022.06.01 |
댓글