Spring/[인프런 김영한 실전 스프링 부트와 JPA 활용 2

[인프런 김영한 실전 스프링 부트와 JPA 활용 2 - API 개발과 성능 최적화] API 개발 고급 - 지연 로딩과 조회 성능 최적화

h2boom 2024. 9. 7. 10:09

지연 로딩과 조회 성능 최적화

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가 조회되는 무한 루프에 빠지게 됨
  • 한 쪽 연관관계에 @JsonIgnore를 사용하고 LAZY 지연 로딩으로 설정된 프록시 객체를 가져오는 과정에서 Json이 처리할 수 없기 때문에 오류가 발생한다.
    • 해결 방법으로 하이버네이트 모듈(Hibernate5JakartaModule)을 등록한다
      => Json에서 지연 로딩으로 인해 조회되지 않은 객체를 로딩을 하지 않고 무시한 채로 Null로 표시한다.
    • 하이버네이트 모듈을 사용하는 방법보다 엔티티 대신 DTO로 변환해서 반환하는 방법이 더 좋다.
    • 하이버네이트 모듈에서 강제 지연 로딩을 지정함으로 프록시 객체를 로딩해서 가져올 수 있는 방법이 있다.
      • 이 방법보다는 직접 프록시 객체에서 메소드에 접근함으로 Lazy를 강제 초기화 시키는 것을 권장한다.
        ex) order.getMember().getName() => Lazy 강제 초기화

 

  • 지연 로딩(LAZY)을 피하기 위해서 즉시 로딩(EAGER)으로 설정하면 안된다!!
    • 즉시 로딩으로 인해 연관관계가 필요 없는 경우에도 데이터를 항상 조회함으로 성능 문제가 발생한다.
    • 지연 로딩은 영속성 컨텍스트에서 조회하기에 이미 조회된 경우 쿼리를 생략하지만 N+1 문제를 해결할 수 없다.
      • ex) Member - Team 양방향 연관관계일 때
        Member 조회 시 쿼리 1번 (2개의 결과가 나왔다고 가정)
        Member -> Team 지연 로딩 조회 N번 (여기서 N=2, 각 Member 2명에 대한 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 변경 시 영향을 받는다.
    • 쿼리 방식 선택 방법
      1. 엔티티로 조회해서 DTO로 변환하는 방법을 우선적으로 선택한다.
      2. 필요한 경우 fetch join으로 성능 최적화를 한다. (이 단계에서 대부분 성능 이슈 해결)
      3. 그래도 안되는 경우 DTO로 조회하는 방법을 사용한다.
      4. 최후의 방법으로 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.

 

  • 레포지토리에는 순수한 엔티티를 조회하는 코드만 들어있고 성능 최적화를 위해서 DTO를 조회하는 코드는 별도의 패키지를 분리해서 작성하는 것이 좋다. => 편한 유지보수를 위함

출처: [인프런 김영한 실전 스프링 부트와 JPA 활용 2 - API 개발과 성능 최적화] 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-API%EA%B0%9C%EB%B0%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94/dashboard

 

실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 강의 | 김영한 - 인프런

김영한 | 스프링 부트와 JPA를 활용해서 API를 개발합니다. 그리고 JPA 극한의 성능 최적화 방법을 학습할 수 있습니다., 스프링 부트, 실무에서 잘 쓰고 싶다면? 복잡한 문제까지 해결하는 힘을 길

www.inflearn.com