본문 바로가기

카테고리 없음

📘 TIL - JPA 성능 최적화 핵심 정리

📌 1. Entity 직접 반환 ❌ → DTO 사용 ✅

이유

  • Entity를 직접 노출하면 JPA 내부 구현에 대한 의존 발생
  • API 스펙이 Entity에 종속 → 유지보수 어려움
  • Lazy 필드 직렬화 문제 (프록시 객체)
  • 유효성 검증, 책임 분리가 어려움

장점

  • 역할 분리 명확
  • 필요한 필드만 노출 가능
  • 보안 및 검증 처리 유리
// 조회 시
@GetMapping("/members")
public List<MemberDto> getMembers() {
    return memberRepository.findAll().stream()
            .map(m -> new MemberDto(m.getId(), m.getName()))
            .collect(Collectors.toList());
}

// 저장 시
@PostMapping("/members")
public ResponseEntity<Void> createMember(@RequestBody CreateMemberRequest request) {
    Member member = new Member();
    member.setName(request.getName());
    memberRepository.save(member);
    return ResponseEntity.ok().build();
}

📌 2. 연관 관계 로딩 전략: Lazy vs Eager

Lazy (지연 로딩)

  • 연관 객체 호출 시 쿼리 실행
  • 초기 쿼리 성능 좋음
  • N+1 문제 주의

Eager (즉시 로딩)

  • 조회 시 무조건 join 실행
  • 예기치 않은 쿼리 증가
  • 복잡한 연관 관계엔 부적합

✅ 기본은 항상 LAZY로!

@ManyToOne(fetch = FetchType.LAZY)
private Member member;

📌 3. 다대일(N:1) 연관관계 N+1 해결

문제 예시

List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
        order.getMember().getName(); // N번 추가 쿼리
}

✅ 해결 방법

1) Fetch Join

@Query("SELECT o FROM Order o JOIN FETCH o.member")
List<Order> findAllWithMember();

 

 

2) EntityGraph

@EntityGraph(attributePaths = {"member"})
@Query("SELECT o FROM Order o")
List<Order> findAllWithMember()

📌 4. DTO 직접 조회 (JPQL new)

장점

  • 필요한 필드만 조회
  • Entity 비의존적 → 가볍고 빠름
  • N+1 회피

예시

@Query("SELECT new com.example.OrderDto(o.id, m.name) FROM Order o JOIN o.member m")
List<OrderDto> findOrderDtos();

📌 5. 일대다(1:N) 연관관계 최적화

❗ 문제: N+1 + 페이징 불가 + 데이터 중복

List<Order> orders = orderRepository.findAll();
orders.get(0).getOrderItems().get(0).getItem().getName(); // 반복 쿼리 발생

✅ 해결 전략

1) Fetch Join + DISTINCT

@Query("SELECT DISTINCT o FROM Order o
JOIN FETCH o.member
JOIN FETCH o.delivery
JOIN FETCH o.orderItems oi
JOIN FETCH oi.item")
List<Order> findAllWithItemsDistinct();

 

  • 모든 연관 객체 한 번에 로딩
  • 페이징 ❌

2) Lazy + @BatchSize (or global config)

 

@BatchSize(size = 100)
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems;

// 또는 application.yml
hibernate.default_batch_fetch_size: 100

 

  • IN 절로 여러 Lazy 객체 한 번에 조회
  • 페이징 가능
  • 유연성 Good

❓ 언제 Fetch Join? 언제 @BatchSize?

                                         

 

구분        Fetch Join  @BatchSize
페이징 ❌ (불가) ✅ (가능)
연관 데이터 수 적을 때 유리 많을 때 유리
API 응답 성격 즉시 모든 데이터 필요한 경우 Lazy 로딩 구조 유지하고 싶을 때
유지보수 복잡도 증가 가능 설정 한 번으로 재사용 가능

🧠 정리

  • 조회 API는 무조건 DTO 반환
  • 연관 관계는 LAZY + 필요 시 fetch join
  • N+1 문제는 fetch join or batch size로 대응
  • 성능에 민감한 API는 DTO 직접 조회로 튜닝