티스토리 뷰

반응형

해당 설명의 소스코드는 링크를 걸어두었습니다.

 

JPA에서는 sql qeury를 한번 감싸놓은 것이기 때문에, 아무렇게나 쓰면 성능에 매우 안좋은 영향을 미칠 수 있다.

특히, select * from [DB name]과 같은 쿼리를 날리게 되면 join 관계가 많은 테이블은 성능이 매우 안좋아 질 수 있다.

 

그럼 가장 간단하지만 성능이 안좋은 방법으로 먼저 진행을 하면

@GetMapping("/api/v1/simple-orders")
public List<Order> orderV1(){
    List<Order> all = orderRepository.findAllByCriteria(new OrderSearch());
    for (Order order: all){
        order.getMember().getName(); // Lazy 강제 초기화
        order.getMember().getAddress(); // Lazy 강제 초기화
    }
    return all;
}

 

Member.java

@Entity
@Table(name = "oreders")
@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {

    @Id @GeneratedValue
    @Column(name = "oreder_id")
    private Long id;

    @JsonIgnore
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) // orderItem을 persist 하게되면 자동으로 persist 해준다.
    private List<OrderItem> orderItems = new ArrayList<>();

    @JsonIgnore
    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate;    // 주문시간

    @Enumerated(EnumType.STRING)
    private OrderStatus status;         // 주문 상태 [ORDER, CANCEL]
    
    /**
     * 생략
     */
}

 

다음과 같이 할 수 있다.

천천히 살펴보면 @JsonIgnore 어노테이션이 Order 클래스의 member 변수에 붙은걸 볼 수 있다.

그 이유로는 양방향으로 서로를 멤버 변수로 가지고 있기 때문에 서로 타고 들어가기 때문에 무한 루프에 빠지게 된다.

왜 무한루프에 빠지게 되냐면, JPA는 아무 설정없이 하나의 Entity가 다른 Entity를 멤버변수로 가지고 있다면,
그 멤버변수 또한 select query를 날리게 된다.
(이 문제는 밑에서 fetch join으로 더 자세하게 다뤄보도록 하자)

 

그리고 Lazy fetch의 경우에는 orders의 변수를 쓰일때에 DB에 접근을 하게 된다.

그렇다면 우선적으로는 임시적으로 ByteBuddyIntercepter를 넣어둔다.

Hibernate5model을 쓰면 해결 가능하나 중요하지 않으니 넘어가도록 하자.

또한, 위 소스코드처럼 Lazy 강제 초기화를 통해서 넣어줄 수 있지만, select query가 너무 많이 나가게 되는 문제가 있다.

 

하지만 저기서 가장 해결해야할 문제로는

Entity가 API를 직접 건드린다는 것이다. 이 것의 문제점은 앞선 장에서 다뤘으니 넘어가도록 하겠다.

 

우선 이 것부터 해결하자면,

 

@GetMapping("/api/v2/simple-order")
public List<SimpleOrderDto> orderV2(){
    return orderRepository.findAllByCriteria(new OrderSearch()).stream()
            .map(o -> new SimpleOrderDto(o))
            .collect(Collectors.toList());
}

 

SimpleOrderDto.java

@Data
public class SimpleOrderDto{
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

    public SimpleOrderDto(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;
    }

    public SimpleOrderDto(Order order){
        this.orderId = order.getId();
        this.name = order.getMember().getName();
        this.orderDate = order.getOrderDate();
        this.orderStatus = order.getStatus();
        this.address = order.getDelivery().getAddress();
    }
}

 

이렇게 해결할 수 있다.

 

하지만 앞선 문제점인 Qeury를 너무 많이 날린다는 것은 변함이 없다.

이것을 바로 N+1문제라고 한다.

 

N+1문제와 fetch join

좀 더 자세하게 설명하면, 위 소스코드에서 order에 1번 쿼리를 날리면

member를 호출하는데 N번, delivery를 호출하는데 N번 이런식으로 주문 리스트를 조회하는데

여러개의 쿼리가 따라오게 되는 문제점을 바로 N+1문제라고 한다.

 

이 것을 해결하는 방법으로는 fetch join이 있는데 그냥 쉽게 생각하면

"SQL의 join 쿼리를 통해 한번에 가져오는 것" 이다. 

사실 SQL을 조금만 해봤더라면 다들 inner join을 생각했을 것이다.

그것을 JPA에서는 다음과 같이 한다.

 

@GetMapping("/api/v3/simple-order")
public List<SimpleOrderDto> orderV3(){
    return orderRepository.findAllWithMemberRepository().stream()
        .map(o -> new SimpleOrderDto(o))
        .collect(Collectors.toList());
}

 

public List<Order> findAllWithMemberRepository() {
    return em.createQuery(
        "select o from Order o" + 
        " join fetch o.member m" + 
        " join fetch o.delivery d", Order.class).getResultList();
}

 

다음처럼 JPQL을 통해서 호출을 해주면 된다.

 

다음 방법은 member, delivery를 모두 불러서 필요한 변수를 DTO로 발라내는 과정이 있지만,

조금더 최적화 하는 방법으로는 

 

public List<SimpleOrderDto> findOrderDtos() {
    return em.createQuery(
        "select new jpabook.jpashop.respoitory.OrderSimpleQeuryDto(o.id, m.name, o.orderDate, o.status, d.address) from Order o" + 
        " join fetch o.member m" + 
        " join fetch o.delivery d", SimpleOrderDto.class).getResultList();
}

 

다음과 같이 애초에 필요한 변수만 발라내어 쿼리를 날리는 방법도 있지만,

재사용성이 떨어지기 때문에 아주 많이 호출되는 api 같은 경우만 따로 파일을 분리해서 만드는 것이 방법이라고 할 수 있다.

728x90
반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함
250x250