📌 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 직접 조회로 튜닝