본문 바로가기

CS 잡지식

[Lazy로딩] 설정으로 인해 발생할 수 있는 치명적인 JPA 버그(매우 매우 중요)[feat. Hibernate5Module]

코드는 https://jbluke.tistory.com/414

 

[양방향] 설정으로 인해 발생할 수 있는 치명적인 JPA 버그(매우 매우 중요)

@RestController @RequiredArgsConstructor public class OrderSimpleApiController { // 이 Controller는 DB에 있는 정보를 [조회]해와서 뿌리는 API용이다. // 즉, 회원 가입 등의 기능을 여기서 필요로 하지 않기에 Service 계

jbluke.tistory.com

를 이어서 설명을 한다. 

[양방향]에 의한 무한 루프 에러는 @JSONIgnore로 해결이 가능했다. 

그러나, 여전히 API를 호출을 해도 에러가 난다. 

HTTPConversionException이 터진다고 나온다. 

지금 이 API는 Order 객체, 정화히는 Order 객체와 그 연관 관계에 있는 객체들(Member,OrderItem, Delivery)에 

[Lazy] 설정을 해 놓았다. 

@Entity
@Table(name = "ORDERS")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {

    @Id@GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="member_id")
    private Member member = new ByteBuddyInterceptor(); // 하이버네이트에서 이 Proxy 객체를 임의로 집어 넣음.

해당 API의 Controller를 보면 알겠지만, Order 객체를 반환하고 있다. 

Order 객체의 구성 요소에는 Member 객체가 있기에 DB에서 해당 Member 객체를 조회해서 Order 객체 안에 넣어서 반환

을 해줘야 정상이다. 

그러나 [LAZY] 설정으로 인해, Member 객체는 지연 로딩, 쉽게 말해서 DB에서 조회가 되지 않은 채 API 호출자한테 반환

이 된다. 

이게 에러의 원인이다. 

하이버네이트(JPA의 구현체)는 Lazy로 인해 Member 객체가 조회되지 않을 경우, 위 코드와 같이 ByteBuddyInterceptor이

라는 Proxy 객체(아마 Member 객체에 대한 Proxy객체일 듯)를 임의로 넣어서 Order 객체를 생성을 한다. 

-> API 호출자에게 JSON이 전달이 됐다고 치자. 

그것들을 파싱하여 화면에 뿌릴려고 하는데, Member 객체는 없고, Proxy 객체(ByteBuddyInterceptor객체)가 있기에

정상적인 파싱이 불가능하여, 에러(HTTPConversionException)가 난다.

Solution : Hibernate5Module이라는 라이브러리를 다운로드 하면, "하이버네트야!! Lazy가 걸려 있어서, Proxy 객체로 대체

된 객체에 대해서는 JSON으로 뿌리지마"라고 명령되서, 알아서 JSON으로 뿌리지 않는다. 

결과적으로, Order 객체 안의 Lazy로 설정되지 않은 연관 관계 객체가 JSON으로 뿌려진다( 그런 객체는 사실 없어야 한다.) 

@SpringBootApplication
public class JpashopApplication {

   public static void main(String[] args) {

      SpringApplication.run(JpashopApplication.class, args);

   }

   @Bean
   Hibernate5Module hibernate5Module(){
      return new Hibernate5Module();
   }

}

Hibernate5Module을 Bean으로 등록을 직접 해줘야 한다. 

 

스프링 부트 3.0 미만:

Hibernate5Module 등록 build.gradle 에 다음 라이브러리를 추가하자

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'

JpashopApplication 에 다음 코드를 추가하자

@Bean

Hibernate5Module hibernate5Module() {

return new Hibernate5Module();

}

기본적으로 초기화 된 프록시 객체만 노출, 초기화 되지 않은 프록시 객체는 노출 안함

만약 스프링 부트 3.0 이상을 사용하면 다음을 참고해서 모듈을 변경해야 한다.

그렇지 않으면 다음과 같은 예외가 발생한다.

java.lang.ClassNotFoundException: javax.persistence.Transient

스프링 부트 3.0 이상: Hibernate5JakartaModule 등록

build.gradle 에 다음 라이브러리를 추가하자 implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5-jakarta'

JpashopApplication 에 다음 코드를 추가하자

@Bean

Hibernate5JakartaModule hibernate5Module() {

return new Hibernate5JakartaModule();

}

(아래와 같이 옵션을 주면, Lazy가 걸려 있어도 DB로 조회를 해와서, JSON으로 [정상] 반환하게 할 수도 있음)

@SpringBootApplication
public class JpashopApplication {

   public static void main(String[] args) {

      SpringApplication.run(JpashopApplication.class, args);

   }

   @Bean
   Hibernate5JakartaModule hibernate5Module(){

      Hibernate5JakartaModule hibernate5JakartaModule = new Hibernate5JakartaModule();
      hibernate5JakartaModule.configure(Hibernate5JakartaModule.Feature.FORCE_LAZY_LOADING,true);
      return hibernate5JakartaModule;
   }

}

 

그러나, 애초에 우리가 API의 결과로서 원헀던 것, Order :: Member와 Order::Delivery 정보였는데,

위와 같이 옵션을 줘버리면 Order 객체 내 Lazy가 걸려 있는 Order :: Member와 Order::Delivery 이외의 연관 관계 객체도

SQL문으로 인해 조회가 되므로, 성능에 문제가 생긴다.(만약 Order 내 Lazy가 걸려 있는 연관 관계 객체가 1000개 라면?)

그리고, 애초에 개발자들이 API를 호출했을 떄 원했던 스펙은 Order :: Member와 Order::Delivery, 이 2개였는데, 

갑자기 다른 스펙의 결과가 나오면 API로 개발하는 개발자들은 망하게 된다. 

 그럼 이거에 대한 Solution은 또 없을까??

있다. 

옵션을 주지 않고도, API를 호출했을 떄 원했던 스펙인 Order :: Member와 Order::Delivery만 출력시키는 방법이 있다.

	@Bean
	Hibernate5JakartaModule hibernate5Module(){

		Hibernate5JakartaModule hibernate5JakartaModule = new Hibernate5JakartaModule();
		//hibernate5JakartaModule.configure(Hibernate5JakartaModule.Feature.FORCE_LAZY_LOADING,true);
		return hibernate5JakartaModule;

	}
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1(){

    //도메인 엔티티를 반환하는 안 좋은 API이다.
    List<Order> findOrders = orderRepository.findAllByString(new OrderSearch());

    //Lazy가 걸린 엔티티를를 사용함으로써 Proxy 객체를 초기화 시켜서 Lazy가 걸린 연관관계 객체를 조회하는 방법도 있다.
    for(Order order : findOrders){
        order.getMember().getName(); //  Order::Member이 강제 초기화!
        order.getDelivery().getAddress(); // Order::Delivery이 강제 초기화!
    }
    return findOrders; // Order::Member과 Order::Delivery만을 반환할 수 있게 되었다.

}
 
API 호출의 결과이다(중간에 NULL로 표시되는 건 어쩔 수가 없다 Hibernate5Module이 null로 처리를 해서 뿌리는 거다)
 
Order::Member과 Order::Delivery만을 반환할 수 있게 되었다.
 
{
        "id"1,
        "member": {
            "id"1,
            "name""userA",
            "address": {
                "city""서울",
                "street""1",
                "zipcode""1111"
            }
        },
        "orderItems"null
        "delivery": {
            "id"1,
            "address": {
                "city""서울",
                "street""1",
                "zipcode""1111"
            },
            "status"null
        },
        "orderDate""2023-05-03T21:46:12.10448",
        "orderStatus""ORDER",
        "totalPrice"50000
    },
    {
        "id"2,
        "member": {
            "id"2,
            "name""userB",
            "address": {
                "city""진주",
                "street""2",
                "zipcode""2222"
            }
        },
        "orderItems"null,
        "delivery": {
            "id"2,
            "address": {
                "city""진주",
                "street""2",
                "zipcode""2222"
            },
            "status"null
        },
        "orderDate""2023-05-03T21:46:12.158493",
        "orderStatus""ORDER",
        "totalPrice"220000
    }