실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 강의 - 인프런
스프링 부트와 JPA를 활용해서 API를 개발합니다. 그리고 JPA 극한의 성능 최적화 방법을 학습할 수 있습니다., 스프링 부트, 실무에서 잘 쓰고 싶다면? 복잡한 문제까지 해결하는 힘을 길러보세요
www.inflearn.com
해당 강의는 Inflearn에 등록된 김영한님의 Spring Boot 강의입니다.
앞의 예제에서는 toOne(OneToOne, ManyToOne) 관계만 있었다. 이번에는 컬렉션인 일대다 관계(OneToMany) 를 조회하고, 최적화하는 방법을 알아보자.
(사실 아래와 같이 주석을 제거하면 강제로 지연로딩으로 바꾸주므로 OrderItems를 조회 할 수 있긴하다.)
@Bean
Hibernate5JakartaModule hibernate5Module() {
Hibernate5JakartaModule hibernate5Module = new Hibernate5JakartaModule();
//강제 지연 로딩 설정(Category등등 전부 LAZY_Loading된것들은 전부 조회해버림-> 성능 저하)
hibernate5Module.configure(Hibernate5JakartaModule .Feature.FORCE_LAZY_LOADING, true);
return hibernate5Module;
}
그러나 지연로딩
으로 조회하면 엔티티의 연관관계를 거쳐서 조회하므로 성능이 매우매우 낮아진다.
아래는 지연로딩 사용에 대한 N+1 문제
의 예이다.
지연 로딩은 연관된 엔티티나 컬렉션(일대다)을 실제로 사용할 때까지 로딩하지 않는 방법이긴하나 N+1문제를 발생시킨다.
예를 들어, Order 엔티티가 여러 개의 OrderItem 엔티티를 가지고 있다고 가정하자.
- Order 리스트를 조회하기 위한 쿼리 1번이 실행된다.
- 리스트의 각 Order에 대해 연관된 _OrderItem들을 로딩하기 위해 N번의 추가 쿼리가 실행_된다. (각 Order마다 1번씩).
1. 주문 조회 V1: 엔티티를 직접노출
OrderApiController
클래스 새로 추가
ordersV1()
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
/**
* V1. 엔티티 직접 노출
* - Hibernate5Module 모듈 등록, LAZY=null 처리
* - 양방향 관계 문제 발생 -> @JsonIgnore
*/
@GetMapping("/api/v1/orders")
public List<Order> ordersV1()
{
List<Order> all = orderRepository.findAllByCriteria(new OrderSearch());
for (Order order : all) {
order.getMember().getName(); //Lazy 강제 초기화,(order.getMember()까지는 프록시 객체)
order.getDelivery().getAddress(); //Lazy 강제 초기화
//oneToMany를 Lazy강제 초기화가 문제임
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName()); //Lazy 강제초기화
}
return all;
}
}
이것은 저번 포스팅에서 OrderItems객체를 스트림으로 바꾼후 for문안에 한번더 foreach로 반복하며 order객체와 연관된 orderItem을 지연로딩 강제초기화를 했다.
위와 같이 프록시 객체 order.getX를 통해 강제 초기화를 진행시켜서 엔티티에 접근한다.
다음 포스팅을 참고하면된다.(기본편:8 프록시와 연관관계 정리 편에서)
https://changuk0308.tistory.com/16
[SpringBoot-JPA 기본편] 자바 ORM 표준 JPA 프로그래밍 - 기본편: 8. 프록시와 연관관계 관리
강의 출처: https://www.inflearn.com/course/ORM-JPA-Basic#curriculum 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의 - 인프런 저는 야생형이 아니라 학자형인가봐요^^ 활용편 넘어갔다 30% 정도 듣고 도저히 답답해
changuk0308.tistory.com
2. 주문 조회 V2: 엔티티를 DTO로 변환
ordersV2
메서드와 OrderDto
,OrderItemDto
/**
* V2. 엔티티를 조회해서 DTO로 변환(fetch join 사용X)
* - 단점: 지연로딩으로 쿼리 N번 호출
*/
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2()
{
List<Order> orders=orderRepository.findAllByCriteria(new OrderSearch());
List<OrderDto> collect=orders.stream()
.map(o-> new OrderDto(o))
.collect(toList());
return collect;
}
@Data
static class OrderDto
{
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
//OrderItem 추가(Dto로 변환)
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();
//OrderItem의 entity를 반환
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();
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
OrderDto에서 OrderItem은 entity
를 반환함. 따라서 Json데이터 형성이 안됨. (키:value
형태가 아니다)
OrderItems를 Dto로 한번더 반환하면 내가 원하는 필드만 노출시킬 수 있고, Json형태의 데이터로 변환 할 수 있다.
결과
최종 orderItem도 원하는 필드만 조회 가능하다.
V2는 지연로딩으로 너무 많은 SQL이 실행하여, 성능이 매우 저하된다.
3. 주문 조회 V3: 엔티티를 DTO로 변환-페치 조인 최적화
OrderApiController
에 추가
/**
* V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용O)
* - fetch join으로 쿼리 1번 호출(Order와 Member join,Order와 Delivery join)
* -> 쿼리를 한개만 보낼 수 있어 성능 최적화 가능!!
*/
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
List<Order> orders=orderRepository.finAllWithItem(); //fetch join
for(Order order: orders)
{
System.out.println("order ref="+order + " id="+order.getId() );
}
List<OrderDto> collect=orders.stream()
.map(o-> new OrderDto(o))
.collect(toList());
return collect;
}
OrderRepository
에 추가
/**
* OrderRepository 클래스(member,delivery,item)
* v3: 엔티티를 페치 조인(fetch join)을 사용해서 쿼리 1번에 조회
*/
public List<Order> finAllWithItem() {
return em.createQuery(
"select distinct o from Order as o" +
" join fetch o.member as m" +
" join fetch o.delivery as d" +
" join fetch o.orderItems as oi" +
" join fetch oi.item as i"
, Order.class)
.getResultList();
}
join fetch전략을 지연로딩으로 인한 N+1문제를 쿼리를 한방에 갖고오면서 해결했다.
쿼리 결과
이렇게 1대 다에서 다만큼 join쿼리가 나감.
distinct사용이유: db의 row를 중복된건 제거해줌.
Distinct를 붙여 같은 엔티티가 조회되면, 애플리케이션에서 중복을 걸러준다.
버전 3의 특징은 다음과 같다.
메모리
에서 페이징
을 해버려서 위험하다. 만약 만개의 데이터가 있으면 쿼리 몇 개만 나가도 out of memory
가 된다.
3.1. 주문 조회 V3.1: 엔티티를 DTO로 변환-페이징과 한계 돌파
한계 돌파 방법
application.yml
jpa: #띄어쓰기 2칸
hibernate: #띄어쓰기 4칸
ddl-auto: create #띄어쓰기 6칸 !!!none or create
properties: #띄어쓰기 4칸
hibernate: #띄어쓰기 6칸
# show_sql: true #띄어쓰기 8칸
format_sql: true #띄어쓰기 8칸
default_batch_fetch_size: 100 #100개까지 갖고옴
ToOne 관계
는 모두 페치 조인
. 나머지는 batch fetch size
를 정하면 된다. 이렇게 함으로써 페이징 기능
수행이 가능하다.
코드
OrderApiController
에 추가
/**
* V3.1 엔티티를 조회해서 DTO로 변환 페이징 고려
* - ToOne 관계만 우선 모두 페치 조인으로 최적화
* - 컬렉션 관계는 hibernate.default_batch_fetch_size, @BatchSize로 최적화
*/
@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_Page(
@RequestParam(value = "offset",defaultValue = "0") int offset,
@RequestParam(value = "limit",defaultValue = "100") int limit)
{
List<Order> orders=orderRepository.findAllWithMemberDelivery(offset,limit); //fetch join(ToOne관계)
List<OrderDto> collect=orders.stream()
.map(o-> new OrderDto(o))
.collect(toList());
return collect;
}
OrderRepository
에 추가
/**
* OrderRepository 클래스(member,delivery,item)
* v3.1: member와 delivery는 페치 조인. 나머지는 쿼리를 한번더 더보냄(다만 페이징 기능 추가)
*/
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
return em.createQuery(
"select o from Order as o" +
" join fetch o.member as m" +
" join fetch o.delivery as d", Order.class)
.setFirstResult(offset) //페이징 조건
.setMaxResults(limit) //페이징 조건
.getResultList();
}
페이징 적용: 1~100의 데이터만 조회-> userA(0번) userB(1번)-> userB만 조회됨.
offset=1로 설정
Offset=0으로 설정하면 userA의 아이템도 들어가는 것을 확인
그전에는 4개의 행 한번에 묶어서 중복이 많게 나가 한 데이터전송량이 많다. 위대로 하면 데이터 중복된 데이터 없이 정교화 된 상태로 쿼리가 나간다. 다만, 네트워크 호출수가 더 많다.
Fetch join+ 페이징의 장점 및 결론
4. 주문 조회 V4: JPA에서 DTO 직접조회
OrderApiController
에 추가
/**
* V4. JPA에서 DTO로 바로 조회
* - 쿼리 1번 호출
* - select 절에서 원하는 데이터만 선택해서 조회
*/
private final OrderQueryRepository orderQueryRepository; //의존관계 주입
@GetMapping("/api/v4/orders")
public List<OrderQueryDto> ordersV4()
{
return orderQueryRepository.findOrderQueryDtos();
}
OrderQueryRepository
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
/**
* 컬렉션은 별도로 조회
* Query: 루트 1번, 컬렉션 N 번
* <p>
* 단건 조회에서 많이 사용하는 방식
*/
public List<OrderQueryDto> findOrderQueryDtos() {
//루트 조회(toOne 코드를 모두 한번에 조회)
List<OrderQueryDto> result = findOrders();
//루프를 돌면서 컬렉션 추가(추가 쿼리 실행)
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems); //가져온 주문 상품(o: OrderItemQueryDto객체)을 setting
});
return result;
}
/**
* 1:N 관계(컬렉션)를 제외한 나머지(ToOne관계..X:1)를 한번에 조회
*/
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 as o" +
" join o.member as m" +
" join o.delivery as 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();
}
}
OrderQueryDto
@Data
@EqualsAndHashCode(of = "orderId") //orderId를 기준으로 Group by 해줌
public class OrderQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate; //주문시간
private OrderStatus orderStatus;
private Address address;
//컬렌셕(N쪽)
private List<OrderItemQueryDto> orderItems;
//생성자(setting)
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;
}
}
OrderItemQueryDto
@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;
}
}
총 쿼리 3번(N+1 문제 발생->ORDER조회1 ORDER_ITEM조회2)
5. 주문 조회 V5: JPA에서 DTO 직접조회-컬렉션 조회 최적화
OrderApiController
/**
* V5. JPA에서 DTO로 바로 조회-컬렉션 조회 최적화
* - 쿼리 2번 호출
* - select 절에서 원하는 데이터만 선택해서 조회
*/
@GetMapping("/api/v5/orders")
public List<OrderQueryDto> orderV5()
{
return orderQueryRepository.findAllByDto_optimization();
}
OrderQueryRepository
에 추가
/**
* 최적화
* Query: 루트 1번, 컬렉션 1번 총 2번
* 데이터를 한꺼번에 처리할 때 많이 사용하는 방식
*/
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;
}
//조회된 주문(Order) 엔티티 리스트에서 주문 ID만을 추출하여 리스트로 반환.
private List<Long> toOrderIds(List<OrderQueryDto> result) {
return result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
}
//주문(Order) ID 리스트를 기반으로 주문 상품(OrderItem)들과 Item을 join해서 조회하고,
//조회된 결과를 주문 ID를 키로 하는 Map 형태로 반환
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();
//람다식으로 루프를 돌리면서 메모리에 MAP을 올려두고 찾음.
return orderItems.stream().collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
}
결과
정리
6. 주문 조회 V6: JPA에서 DTO 직접조회,플랫 데이터 최적화
OrderApiController
에 추가
/**
* V6. JPA에서 DTO로 바로 조회,플랫 데이터 최적화
* - 쿼리 1번 호출
* - 중복데이터 추가 및 페이징 불가능
*/
@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ordersV6() {
List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();
//FlatDto를 OrderDto로 반환
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
에 생성자 추가
//FlatDto를 위한 생성자
public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address,
//OrderItem에 대한 setting값 추가){
List<OrderItemQueryDto> orderItems) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
this.orderItems = orderItems;
}
OrderQueryRepository
에 추가
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();
OrderFlatDto
생성
@Data
public class OrderFlatDto {
private Long orderId;
private String name;
private LocalDateTime orderDate; //주문시간
private Address address;
private OrderStatus orderStatus;
//order_item
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;
}
}
정리
단순OrderFlatDto
를 반환시 OrderItem
에 대한 키값
이 없어짐.(즉,API 스펙
이 사라짐.)
Api 스펙
생성을 위해서 Controller에서 맞춰준다. 즉, 내가 직접 중복을 걸러내고, FlatDto
를 OrderDto
로 루프를 돌며 매핑
해서 API 스펙을 맞춘다. 이렇게v6
는 쿼리도 한방으로 뽑아 낼 수 있다.
결과
7. API 개발 고급 정리
엔티티 조회 (엔티티 생성후 엔티티를 DTO로 변환하여 조회)
- 엔티티 조회해서 그대로 반환 : V1
- 엔티티 조회 후 DTO로 변환 : V2
- 페치 조인으로 쿼리 수 최적화 : V3
- 컬렉션 페이징과 한계 돌파 : V3.1
- 컬렉션은 페치 조인시 페이징이 불가능
- ToOne관계는 페치 조인으로 쿼리 수 최적화
- 컬렉션은 페치 조인 대신에 지연 로딩을 유지하고, hibernate.default_batch_fetch_size, @BatchSize로 최적화
DTO 직접 조회
- JPA에서 DTO를 직접 조회 : V4
- 컬렉션 조회 최적화 - 일대다 관계인 컬렉션은 IN 절을 활용해서 메모리에 미리 조회해서 최적화 : V5
- 플랫 데이터 최적화 - JOIN 결과를 그대로 조회 후 애플리케이션에서 원하는 모양으로 직접 변화 : V6
권장 순서
- 엔티티 조회 방식으로 우선 접근
- 페치조인으로 쿼리 수를 최적화
- 컬렉션 최적화
- 페이징 필요 hibernate.default_batch_fetch_size 로 최적화
- 페이징 필요 X → fetch join 사용
- 엔티티 조회 방식으로 해결이 안되면 DTO 조회 방식 사용
- DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate
참고 : 엔티티 조회 방식은 페치 조인이나, hibernate.default_batch_fetch_size , @BatchSize r같이 코드를 거의 수정하지 않고, 옵션만 약간 변경해서, 다양한 성능 최적화를 시도할 수 있다. 반면에 DTO를 직접 조회하는 방식은 성능을 최적화하거나, 성능 최적화 방식을 변경할 때 많은 코드를 변경해야 한다.
참고 : 개발자는 성능 최적화와 코드 복잡도 사이에서 줄타기를 해야 한다. 항상 그런 것은 아니지만, 보통 성능 최적화는 단순한 코드를 복잡한 코드로 몰고간다. 엔티티 조회 방식은 JPA가 많은 부분을 최적화 해주기 떄문에, 단순한 코드를 유지하면서, 성능을 최적화 할 수 있다. 반면에, DTO 조회 방식은 SQL을 직접 다루는 것과 유사하기 때문에, 둘 사이에 줄타기를 해야 한다.
DTO 조회 방식의 선택지
- DTO로 조회하는 방법도 각각 장단이 있다. V4,V5,V6에서 단순하게 쿼리가 1번 실행된다고 V6이 상항 좋은 방법인 것은 아니다
- V4는 코드가 단순하다. 특정 주문 한 건만 조회하면 이 방식을 사용해도 성능이 잘 나온다. 예를 들어서 조회한 Order 데이터가 1건이면 OrderItem을 찾기 위한 쿼리도 1번만 실행하면 된다.
- V5는 코드가 복잡하다. 여러 주문을 한꺼번에 조회하는 경우에는 V4 대신에 이것을 최적화한 V5 방식을 사용해야 한다. 예를 들어서 조회한 Order 데이터가 1000건인데, V4 방식을 그대로 사용하면, 쿼리가 총 1 + 1000번 실행된다. 여기서 1은 Order 를 조회한 쿼리고, 1000은 조회된 Order의 row 수다. V5 방식으로 최적화 하면 쿼리가 총 1 + 1번만 실행된다. 상황에 따라 다르겠지만 운영 환경에서 100배 이상의 성능 차이가 날 수 있다.
- V6는 완전히 다른 접근방식이다. 쿼리 한번으로 최적화 되어서 상당히 좋아보이지만, Order를 기준으로 페이징이 불가능하다. 실무에서는 이정도 데이터면 수백이나, 수천건 단위로 페이징 처리가 꼭 필요하므로, 이 경우 선택하기 어려운 방법이다. 그리고 데이터가 많으면 중복 전송이 증가해서 V5와 비교해서 성능 차이도 미비하다.
종합
<엔티티 조회 방식과 DTO 조회 방식의 선택 기준>
1. 엔티티 조회 방식은 코드의 단순함을 유지하면서 JPA의 다양한 기능(예: 페치 조인, 배치 사이즈 설정 등)을 활용해 쿼리 수를 최적화하고 성능을 개선할 수 있는 경우에 적합하다. 이 방법은 코드 변경을 최소화하며 성능 최적화를 시도할 수 있다는 장점이 있다.
2. DTO 조회 방식은 성능 최적화가 필수적이거나, 클라이언트에 특정 데이터만 전달해야 할 때 유리하다. 이 방식은 SQL을 직접 조작하는 것과 유사해 세밀한 최적화가 가능하지만, 성능 최적화를 위해 더 많은 코드 수정이 필요할 수 있다는 단점이 있다.
<성능 최적화 순서>
1. 페치 조인 사용: 연관된 엔티티를 한 번의 쿼리로 불러와 쿼리 수를 줄이는 것이 우선이다.
2. 컬렉션 최적화: `ToOne` 관계는 페치 조인으로 최적화하고, `ToMany` 관계는 `hibernate.default_batch_fetch_size` 또는 `@BatchSize`를 통해 최적화한다.
3. 페이징 최적화: 필요한 경우 `hibernate.default_batch_fetch_size`로 페이징 성능을 개선한다. 페이징이 필요 없는 경우 페치 조인을 사용한다.
4. DTO 조회 방식 고려: 엔티티 조회 방식으로 해결되지 않는 경우 DTO 조회 방식을 고려한다. 이때도 최적화가 필요하면 네이티브 SQL이나 스프링 JdbcTemplate을 사용할 수 있다.
<DTO 조회 방식의 선택지>
- V4 (단순 DTO 조회): 코드가 단순하며 특정 주문 한 건만 조회할 때 적합하다.
- V5 (컬렉션 조회 최적화): 여러 주문을 한꺼번에 조회할 때 쿼리 수를 대폭 줄여 성능을 크게 향상시킨다.
- V6 (플랫 데이터 최적화): 쿼리 한 번으로 데이터를 조회하지만, 페이징이 불가능하고 중복 데이터 전송이 증가할 수 있다.
결론
API 개발 시 성능 최적화와 코드 복잡도 사이에서의 균형이 중요하다. 가능한 한 엔티티 조회 방식을 우선 고려하되, 성능 문제가 해결되지 않을 때 DTO 조회 방식으로 전환하는 것이 권장된다. 각 상황에 따라 최적의 방식을 선택하여, 성능과 유지보수성 사이에서 최적의 균형을 찾아야 한다.