[Spring] JPA 최적화와 성능 향상

Neo-Renaissance·2025년 1월 8일

JPA를 사용한 API 개발에서 성능 최적화효율적인 데이터 처리는 실무의 핵심입니다. 이번 글에서는 실무에서 꼭 알아야 할 JPA의 최적화 기술을 총망라하여 소개합니다. DTO 변환, Fetch Join, N+1 문제 해결, 배치 페칭 등 필수적인 주제들을 구체적 사례와 함께 다룹니다.


1. 엔티티 직접 노출의 문제점

문제점

  • 엔티티 노출:
    • 엔티티를 직접 반환하면 모든 필드가 외부에 노출됩니다.
    • 변경 시 API 스펙이 함께 변합니다.
  • 양방향 연관관계:
    • 무한 루프를 방지하기 위해 @JsonIgnore 같은 설정이 필요합니다.

해결책

  • DTO 변환:
    • API 스펙에 맞는 DTO를 생성하여 반환합니다.
    • 엔티티와 API의 독립성을 유지합니다.

2. N+1 문제와 해결 방법

N+1 문제란?

  • JPA의 지연 로딩(Lazy Loading)으로 인해 추가 쿼리가 발생하는 문제입니다.
  • 예: Order 10건을 조회하면, 각 OrderMemberOrderItem을 조회하기 위해 10+N번의 쿼리가 실행됩니다.

해결 방법

  1. Fetch Join:

    • 연관 데이터를 한 번의 쿼리로 가져옵니다.
    @Query("select o from Order o join fetch o.member join fetch o.delivery")
    List<Order> findAllWithMemberDelivery();
  2. 배치 페칭:

    • @BatchSize 또는 hibernate.default_batch_fetch_size 설정으로 최적화.
    @BatchSize(size = 100)
    private List<OrderItem> orderItems;

3. DTO 직접 조회

장점

  • 필요한 데이터만 조회하여 전송량 감소.
  • 쿼리를 작성하면서 데이터 형태를 명확히 정의 가능.

예시

@Query("select new com.example.OrderDto(o.id, m.name, d.address) from Order o join o.member m join o.delivery d")
List<OrderDto> findOrderDtos();

한계

  • API 스펙에 맞춘 쿼리 작성으로 재사용성 감소.

4. V3.1 방식: 엔티티 조회 + 페이징 최적화

특징

  • ToOne 관계는 Fetch Join으로 최적화.
  • ToMany 관계는 지연 로딩으로 가져오되, 배치 페칭 적용.

예시

public List<Order> findAllWithMemberDelivery(int offset, int limit) {
    return em.createQuery("select o from Order o join fetch o.member join fetch o.delivery", Order.class)
             .setFirstResult(offset)
             .setMaxResults(limit)
             .getResultList();
}

장점

  • 페이징 가능.
  • N+1 문제 해결.

한계

  • Fetch Join을 남용하면 성능 저하 가능.

5. V4 방식: DTO로 직접 조회 + 컬렉션 조회

Query 실행 방식

  1. 루트 데이터(ToOne 관계)를 조회.
  2. 컬렉션 데이터(ToMany 관계)를 추가 쿼리로 조회.

예시

public List<OrderQueryDto> findAllByDto() {
    List<OrderQueryDto> result = findOrders();
    result.forEach(o -> {
        List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
        o.setOrderItems(orderItems);
    });
    return result;
}

장점

  • ToOne 관계 최적화.
  • DTO를 활용한 명확한 데이터 정의.

단점

  • N+1 문제 발생 가능.

6. V5 방식: DTO 조회 + 컬렉션 최적화

특징

  • Query 1번: 루트 조회.
  • Query 1번: 컬렉션 조회.

예시

public List<OrderQueryDto> findAllByDto_optimization() {
    List<OrderQueryDto> result = findOrders();
    Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result));
    result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
    return result;
}

장점

  • 쿼리 호출 수 감소.
  • N+1 문제 해결.

단점

  • 쿼리 작성 복잡도 증가.

7. V6 방식: 플랫 데이터 최적화

특징

  • 모든 데이터를 한 번의 쿼리로 조회.
  • 플랫 데이터를 애플리케이션에서 그룹핑하여 계층 구조로 변환.

예시

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();
}

장점

  • 쿼리 호출 1번으로 모든 데이터 조회.

단점

  • 중복 데이터 전송.
  • 애플리케이션에서 추가 작업 필요.
  • 페이징 불가.

8. JPA 최적화 전략 요약

방식특징장점단점
V3.1엔티티 조회 + 페이징 가능N+1 문제 해결, 페이징 가능Fetch Join 남용 시 성능 저하 가능
V4DTO 조회 + 추가 쿼리 실행DTO 활용으로 명확한 데이터 정의N+1 문제 발생 가능
V5DTO 조회 + 컬렉션 최적화쿼리 호출 수 감소, N+1 문제 해결쿼리 작성 복잡
V6플랫 데이터 + 애플리케이션 처리쿼리 1번 실행중복 데이터 전송, 페이징 불가

9. 결론

  • 실무에서는 상황에 맞는 전략 선택이 중요합니다.
    • 작은 데이터셋: V6.
    • 페이징 필요: V3.1.
    • 성능 최적화: V5.
  • JPA의 강력한 기능을 제대로 활용하면, 효율적이고 유지보수 가능한 API를 개발할 수 있습니다.

JPA 최적화를 이해하고 실무에 적용하여, 고성능 API를 구축해 보세요!

profile
if (실패) { 다시 도전; } else { 성공; }

0개의 댓글