똑같은 삽질은 2번 하지 말자
Spring+JPA REST API 성능 최적화 본문
잘 정리된 PDF파일이 있어서 작성 안할려고 했는데,
뭔가 문서화를 내 손으로 안하면 찜찜해서 조금이라도 적어보려고 한다.
이번 강의에서는 REST API를 개발할 때, 성능최적화를 하는 방법들에 대해서 인데,
제일 중요한건 엔티티를 웹에 노출해서는 안된다!
솔직히 이 한마디로 이번 강의 전체가 들어 있다고 보면 될꺼같다..
그럼 어떻게 해야하나? 간단하다. 별도의 DTO를 만들어서 거기서 조회한 엔티티를 넣어서
반환해 주면 된다.
ex)
// 조회 API
@GetMapping("/api/v2/members")
public Result membersV2() {
List<Member>findMembers = memberService.findMembers();
List<MemberDto>collect = findMembers.stream()
.map(m -> new MemberDto(m.getName()))
.collect(Collectors.toList());
return new Result(collect,collect.size());
// 이렇게 클래스로 감싸야 안에 형식을 바꾸는게 유연해진다.
}
@Data
@AllArgsConstructor
class Result<T> {
private T data;
}
@Data
@AllArgsConstructor
class MemberDto {
private String name;
}
위 Result로 감싼 이유는 Json객체 Json배열의 차이인데 객체에는 속성 추가하는게 가능해서
유연하게 값을 반환할 수 있기때문이다. 그대로 List로 보내면 Array로 반환되기 때문에 Array라고 하는건
똑같은 값을 형태가 반복되는 자료형이라는건 알고있겠지
1:1 조회 API의 경우의 성능최적화를 위해서는
1. 패치 조인(fetch join)을 사용해라!(1-> N 쿼리 실행 문제 해결)
2. 그리고 또 제일 중요한건 엔티티를 웹에 노출해서는 안된다!
3. 연관 관계일때, 한곳을 @JsonIgnore처리 해야 한다.
@Data
static class SimpleOrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate; //주문시간
private OrderStatus orderStatus;
private Address address;
public SimpleOrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName(); // 지연로딩 초기화
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); // 지연로딩 초기화
}
}
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithMemberDelivery();
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(toList());
return result;
}
//OrderRepository 추가 코드
public List<Order> findAllWithMemberDelivery() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.getResultList();
}
신기하네 패치조인...이런식으로 하면 원래 주문2개일경우 쿼리가 5번날라가는데,
이 케이스에서는 쿼리가 단 한번 날라간다
쿼리 방식 선택 권장 순서
1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
2. 필요하면 페치 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결된다.
3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사 용한다.
1:N 조회 API의 성능최적화 할 경우!
toOne 관계는 모두 fetchJoin하는데,
toMany 관계는 페치조인을 하면,
페치 조인으로 SQL이 1번만 실행됨 distinct를 사용한 이유는 1대다 조인이 있으므로
데이터베이스 row가 증가한다. 그 결과 같은 order 엔티티의 조회 수도 증가하게 된다.
JPA의 distinct는 SQL에 distinct를 추가하고, 더해서 같은 엔티티가 조회되면, 애플리케이션에서 중복을 걸러준다.
이 예에서 order가 컬렉션 페치 조인 때문에 중복 조회 되는 것을 막아준다.
단점 페이징 불가능
> 참고: 컬렉션 페치 조인을 사용하면 페이징이 불가능하다. 하이버네이트는 경고 로그를 남기면서 모든 데이 터를 DB에서 읽어오고, 메모리에서 페이징 해버린다(매우 위험하다).
> 참고: 컬렉션 페치 조인은 1개만 사용할 수 있다. 컬렉션 둘 이상에 페치 조인을 사용하면 안된다. 데이터가 부정합하게 조회될 수 있다.
한계 돌파
그러면 페이징 + 컬렉션 엔티티를 함께 조회하려면 어떻게 해야할까?
1. 먼저 ToOne(OneToOne, ManyToOne) 관계를 모두 페치조인 한다.
ToOne 관계는 row수를 증가시 키지 않으므로 페이징 쿼리에 향을 주지 않는다.
2. 컬렉션(ToMany)은 지연 로딩으로 조회한다.
지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size, @BatchSize를 적용한다.
hibernate.default_batch_fetch_size: 글로벌 설정
@BatchSize: 개별 최적화
이 옵션을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회한다.
그럼 1 N N N N 인 데이터를 조회한다고하면 1 1 1 1 1 로 되는거다..
그리고 ToMany까지 페치조인을 하고나서 가져온 데이터를 보면 중복이 너무 많은 아주 커다란 친구로 되어있는데,
지연로딩으로 따로 가져오면 위의 친구에서 정규화된 데이터가 되어있는 장점도 있다.
지연 로딩한 예시
@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);
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
@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(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();
}
}
'Spring' 카테고리의 다른 글
Lombok(롬복)은 어떻게 동작하는 걸까? (0) | 2020.09.19 |
---|---|
Java(Enum) → DB(Int) (0) | 2020.06.16 |
@PathVariable 사용해보자(전달인자 처리) (0) | 2020.06.07 |
Redirect parameter ? How are you going to spend it? (0) | 2020.04.23 |
Jsoup 설치 & Test (0) | 2020.04.16 |