지연 로딩과 조회 성능 최적화
public class Member {
...
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "team_id")
private List<Team> team = new ArrayList<>();
}
public class Team {
...
@JsonIgnore
@OneToMany(mappedBy = team)
private Member member;
}
- 엔티티를 외부에 직접 노출할 때, 양방향 연관관계가 걸린 곳은 Json 타입으로 데이터를 가져올 때 한 쪽 연관관계에 반드시 @JsonIgnore를 사용해야 한다.
- 그렇지 않으면 Json 생성 시 서로 계속 참조하게 되면서 무한 루프에 빠지게 된다.
- 엔티티를 외부에 직접 노출시키지 않는 것이 가장 좋다.
- ex) Member - Team 양방향 연관관계일 때
Member를 조회하면 Team이 조회되고 Team이 조회되면 Team의 Members가 조회되는 무한 루프에 빠지게 됨
- 그렇지 않으면 Json 생성 시 서로 계속 참조하게 되면서 무한 루프에 빠지게 된다.
- 한 쪽 연관관계에 @JsonIgnore를 사용하고 LAZY 지연 로딩으로 설정된 프록시 객체를 가져오는 과정에서 Json이 처리할 수 없기 때문에 오류가 발생한다.
- 해결 방법으로 하이버네이트 모듈(Hibernate5JakartaModule)을 등록한다
=> Json에서 지연 로딩으로 인해 조회되지 않은 객체를 로딩을 하지 않고 무시한 채로 Null로 표시한다. - 하이버네이트 모듈을 사용하는 방법보다 엔티티 대신 DTO로 변환해서 반환하는 방법이 더 좋다.
- 하이버네이트 모듈에서 강제 지연 로딩을 지정함으로 프록시 객체를 로딩해서 가져올 수 있는 방법이 있다.
- 이 방법보다는 직접 프록시 객체에서 메소드에 접근함으로 Lazy를 강제 초기화 시키는 것을 권장한다.
ex) order.getMember().getName() => Lazy 강제 초기화
- 이 방법보다는 직접 프록시 객체에서 메소드에 접근함으로 Lazy를 강제 초기화 시키는 것을 권장한다.
- 해결 방법으로 하이버네이트 모듈(Hibernate5JakartaModule)을 등록한다
- 지연 로딩(LAZY)을 피하기 위해서 즉시 로딩(EAGER)으로 설정하면 안된다!!
- 즉시 로딩으로 인해 연관관계가 필요 없는 경우에도 데이터를 항상 조회함으로 성능 문제가 발생한다.
- 지연 로딩은 영속성 컨텍스트에서 조회하기에 이미 조회된 경우 쿼리를 생략하지만 N+1 문제를 해결할 수 없다.
- ex) Member - Team 양방향 연관관계일 때
Member 조회 시 쿼리 1번 (2개의 결과가 나왔다고 가정)
Member -> Team 지연 로딩 조회 N번 (여기서 N=2, 각 Member 2명에 대한 Team을 조회)
- ex) Member - Team 양방향 연관관계일 때
- 항상 지연 로딩을 기본으로 하되 성능 최적화가 필요한 경우 패치 조인(fetch join)을 사용할 것!
- 즉시 로딩은 어떤 상황에서도 사용하지 말 것!
//fetch join 예시 Member - Team
em.create("select m from Member m join fetch m.team", Member.class);
- 지연 로딩으로 설정되어 있음에도 fetch join으로 Member -> Team은 이미 조회된 상태이므로 쿼리 1번으로 Member와 Team을 모두 조회할 수 있다.
=> fetch join을 사용함으로 지연로딩이 발생하지 않으며 N+1 문제 해결할 수 있다.
- DTO 설계 시 엔티티를 참조하는 것은 괜찮다.
- 구체적이지 않은 것이 구체적인 것에 의지하는 것은 괜찮지만 반대가 되면 안된다.
public List<OrderSimpleQueryDto> findOrderDtos() {
return em.createQuery("select new jpabook.jpashop.repository.OrderSimpleQueryDto(o.id,m.name,o.orderDate,o.status,d.address) " +
"from Order o " +
"join o.member m " +
"join o.delivery d", OrderSimpleQueryDto.class)
.getResultList();
}
- 기본적으로 JPA는 엔티티나 ValueObject만 반환할 수 있다.
- DTO를 JPA로 반환하기 위해서 new 연산자를 사용해야한다.
- JPA에서 엔티티 조회 vs JPA에서 DTO 조회
- 엔티티로 조회 시 - 쿼리가 엔티티 전체를 조회하지만 엔티티로 조회 후 DTO로 변경하는 경우 코드의 재사용성이 더 높고 데이터를 수정할 수 있다.
- DTO로 조회 시 - 필요한 컬럼만 조회함으로 SELECT절이 줄어들어 약간의 애플리케이션 네트워크 용량을 최적화 할 수 있지만 해당 DTO에서만 사용할 수 있기에 재사용성이 떨어지고 데이터를 수정할 수 없다.
=> 레포지토리에 API 스펙에 의존해서 설계하기 때문에 API 변경 시 영향을 받는다. - 쿼리 방식 선택 방법
- 엔티티로 조회해서 DTO로 변환하는 방법을 우선적으로 선택한다.
- 필요한 경우 fetch join으로 성능 최적화를 한다. (이 단계에서 대부분 성능 이슈 해결)
- 그래도 안되는 경우 DTO로 조회하는 방법을 사용한다.
- 최후의 방법으로 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.
- 레포지토리에는 순수한 엔티티를 조회하는 코드만 들어있고 성능 최적화를 위해서 DTO를 조회하는 코드는 별도의 패키지를 분리해서 작성하는 것이 좋다. => 편한 유지보수를 위함
출처: [인프런 김영한 실전 스프링 부트와 JPA 활용 2 - API 개발과 성능 최적화]
실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 강의 | 김영한 - 인프런
김영한 | 스프링 부트와 JPA를 활용해서 API를 개발합니다. 그리고 JPA 극한의 성능 최적화 방법을 학습할 수 있습니다., 스프링 부트, 실무에서 잘 쓰고 싶다면? 복잡한 문제까지 해결하는 힘을 길
www.inflearn.com