코딩과 결혼합니다

[JPA] 지연 로딩과 조회 성능 최적화(1) 본문

2세/JPA

[JPA] 지연 로딩과 조회 성능 최적화(1)

코딩러버 2024. 2. 19. 16:07
728x90
등록이나 수정 등은 데이터 한 건을 다루는 것이기 때문에 거의 성능 문제가 발생하지 않는다. 주로 조회하는 데에 문제가 발생하는데 이번에는 여러 방법으로 데이터를 조회해 보면서 JPA로 성능을 최적화하는 방법을 알아본다.

 

@GetMapping("/api/v1/simple-orders")
public List<Order> orderV1() {
    List<Order> all = orderRepository.findAllByString(new OrderSearch());
    return all;
}

아주 간단한 주문 조회 API이다. Order Entity를 그대로 사용하며 Order 안에는 Member와 Delivery가 포함되어 있다.

 

무한루프

이렇게 조회를 하게 되면 어떤 문제가 생기게 될까?

public class Order {
	...

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
    
    @OneToOne(fetch = LAZY, cascade = ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;
    
    ...
    }

Order는 memver의 정보를 가져와야 하므로 member를 참조한다. 

@Entity
@Getter @Setter
public class Member {

	...

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}

그런데 이 meber에도 orders 가 존재한다. 그럼 member는 orders를 참조하며 무한 루프에 빠진다.

 

이를 해결하는 방법으로 @JsonIgnore를 사용한다. 양방향 관계일 때 둘 줄 하나에 걸어주는 것이다.

    @JsonIgnore
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

 Order 객체를 포함한 곳에 위와 같이 @JsonIgnore를 걸어주었다.

 

Type Definition Error

하지만 간단하게 해결되지 않는다. 멤버와 딜리버리에 지연로딩이 걸려있기 때문이다.

 "timestamp": "2024-02-19T06:19:43.705+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "trace": "org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]\r\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:489)\r\n\tat org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:114)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.

 

  1. 지연로딩은 new 객체를 해서 가져오지 않는다. 즉, Order의 데이터만 가지고 오고 멤버나 딜리버리의 데이터는 손대지 않는다.
  2. member에 null 값을 넣을 수는 없기 때문에, 하이버네이트에서는 멤버를 상속받은 가짜 프록시 객체를 생성해서 넣어놓는다. 이것을 byteBuddy라 한다.
  3. 제이슨이 루프를 돌리며 오더를 가지고 멤버를 뽑아 보려 하는데 멤버가 아니고 bytebuddy라는 것이 존재하며, 이것을 어떻게 처리할 수가 없어서 오류가 난 것이다.
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'

Spring 버전 3.0이상
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5-jakarta'

+ 나는 하이버네이트 버전이 6이상이라서
'com.fasterxml.jackson.datatype:jackson-datatype-hibernate6' 를 사용해주었다.
버전 간의 호환성 문제를 방지하기 위해서이다.

 

bulid.gradle에 하이버네이트 6 모듈을 깔아주고 이를 사용해

지연로딩인 경우 제이슨라이브러리가 위와 같이 어떠한 행동을 하지 못하도록 해준다.

@SpringBootApplication
public class JpashopApplication {

    public static void main(String[] args) {
        SpringApplication.run(JpashopApplication.class, args);
    }

    @Bean
    Hibernate6Module hibernate6Module() {
        return new Hibernate6Module();
    }
}

 

 

하지만 지연로딩이 걸린 것들은 모두 null을 반환한다.

DB에서 조회한 것이 아니기 때문에 제이슨이 다 무시해 버리기 때문이다.

@Bean
Hibernate6Module hibernate6Module() {
    Hibernate6Module hibernate6Module = new Hibernate6Module();
    //강제 지연 로딩 설정
    hibernate6Module.configure(Hibernate6Module.Feature.FORCE_LAZY_LOADING,
            true);
    return hibernate6Module;
}

옵션을 통해서 강제로 레이지로딩을 해버려 값을 가져올 수 있다.

 

성능저하

이건 어떤 식으로 동작하는지 스스로 이해하기 위해 적어보았지만, 사실 애초에 엔티티 자체를 노출하는 것은 좋지 않기 때문에 자세히 알고 있을 필요는 없다. 

 

나는 멤버와 딜리버리 정보만 필요했지만 이런 식으로 코드를 짜게 되면 굉장한 성능 낭비도 이뤄진다.

  • 필요 없는 정보까지 노출 또한 DB에서 추가로 끌고 오기 때문에 쿼리가 나간다.
  • 포스 레이지로딩으로 레이지로딩된 애들을 모두 가져오며 어마어마한 쿼리가 나가게 된다.
@GetMapping("/api/v1/simple-orders")
public List<Order> orderV1() {
    List<Order> all = orderRepository.findAllByString(new OrderSearch());
    for (Order order : all) {
        order.getMember().getName();//Lazy 강제 초기화
        order.getDelivery().getAddress();//Lazy 강제 초기화
    }
    return all;
}

 

이를 해결하는 방법이 있긴하다. 강제 지연 로딩하는 옵션을 주석처리하고 name, adrress를 꺼내 옴으로 강제로 지연 로딩을 초기화한다. 강제 초기화가 된 것은 데이터를 가지고 있기 때문에 정상적으로 잘 출력이 된다. 

 

그러나 여전히 member를 보면 다른 필요 없는 엔티티까지 모두 포함한다. 이를 이미 외부에서 써버리게 된다면

나중에 그 내용을 하나 바꾸려 할 때에 문제가 생긴다.


📌지연로딩(LAZY)을 피하기 위해 즉시로딩(EARGR)으로 설정하면 안된다! 즉시 로딩 때문에 연관관계가 필요없는 경우에도 데이터를 항상 조회하기 때문에 성능 문제가 발생할 수 있다. 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워 진다.

항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 패치 조인(fetch join)을 사용한다.