[이슈해결] Lazy Loading으로 인한 HibernateProxy 객체에 대한 오해

MinSeong Kang·2022년 9월 12일
0

이슈해결

목록 보기
6/12

프로젝트 진행시 연관관계로 맺어있는 객체를 Getter 메서드를 통해 가져왔지만, 객체 내의 필드값들이 모두 null값인 상황이 발생했다.

public class Cart extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "CART_ID")
    private Long id;

    private String userNickName;
    private int count;
    private boolean isOrdered;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "MENU_ID")
    private Menu menu;

    @OneToMany(mappedBy = "cart", cascade = CascadeType.ALL)
    private List<CartAndOption> cartAndOptions = new ArrayList<>();
}
public class Menu extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MENU_ID")
    private Long id;

    @NotEmpty
    private String name;
    @NotNull
    private int price;
    
    // 생략
}

엔티티의 어노테이션과 엔티티 내 편의 메서드는 생략하였다.

@Service
@RequiredArgsConstructor
public class CartService {
	// 생략
  public CartDetailDto getCartDetailDtoById(Long id) {
      Optional<Cart> findCart = cartRepository.findById(id);

      if (findCart.isEmpty()) {
          throw new CartNotFoundException("해당 장바구니는 존재하지 않습니다.");
      }

      Cart cart = findCart.get();
      Menu menu = cart.getMenu();
      // 생략
  }
}

해당 메서드는 CartService에 있는 메서드이며, Cart의 id 값을 통해 CartDetailDto를 생성하여 반환하는 메서드이다.

프로젝트 개발 당시, cartTRepository.findById(id)를 통해 Cart 객체를 조회하고, cart.getMenu();를 통해 Cart와 연관되어 있는 Menu 객체를 조회할 수 있다고 생각하여 해당 로직을 작성하였다. 하지만 조회한 Cart객체의 menu객체의 모든 필드는 null로 되어있었다.

처음에는 menu 객체가 제대로 생성되지 않은 채 Cart 객체에 저장해주었다고 생각을 해서, Cart객체를 생성하는 로직을 찾아 살펴보았다. 하지만 저장시에는 아무런 문제가 없었고, 디버깅시 제대로 menu객체가 저장되는 것을 확인할 수 있었다.

Cart 엔티티를 조회하고 Cart 엔티티 내 Menu객체의 모든 필드가 null로 되어 있던 원인은 바로 지연로딩으로 설정해주었기 때문이었다.

 @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "MENU_ID")
    private Menu menu;

엔티티 초기 설계시 성능측면을 위해 모든 즉시로딩을 지연로딩으로 변경해주었다. 이로 인해 엔티티 조회시 연관되어 있는 객체들은 실제 사용하는 시점에 데이터베이스에 조회를 할 수 있었다. 하지만...

필자는 이 말이 당연히 cart.getMenu()를 호출했을 때 menu를 직접 사용하는 시점이니, menu객체의 필드값이 모두 채워질 것이라고 생각했다.

따라서 cart.getMenu().getName()을 사용하는 시점에 menu객체의 필드는 모두 null이었기 때문에 cart.getMenu().getName() 조회시 null 값을 가져온다고 생각을 하였다. 그래서 CartDetailDto 객체를 만들 때 필드 값이 제대로 저장되지 않을 것이라고 생각을 했다. 따라서 fetch join을 사용해서 Cart 엔티티를 가져올 때 Menu 객체도 같이 조회하도록 구현을 하였다.

하지만 이 모든 것은 오해였다..!!!!!

fetch join을 사용하지 않았도 CartDetailDto 객체의 필드값이 제대로 잘 저장되어 CartDetailDto를 반환할 수 있었다.

다음과 같이 cart.getMenu().getName()을 하는 순간 menu$HibernateProxy의 target이 menu의 정보를 가지고 있었다. 아래의 그림을 보면 더더욱 이해가 잘 될 것 같다. 저의 엔티티로 해당 그림을 해석하면,

  • 클라이언트에서 Cart를 조회했을 당시에는 지연로딩이기 때문에, Cart는 Menu는 Menu$HibernateProxy로 가지고 있게 된다.
  • 클라이언트가 cart.getMenu().getName()을 하는 시점에, 초기화 요청을 보내고 영속성 컨텍스트를 통해 데이터베이스를 조회한다.
  • 조회한 값을 통해 실제 Menu 엔티티 객체를 생성하고, Menu target이 Menu 엔티티 객체에서 값을 가져오도록 한다.

따라서 지연로딩을 사용하면 실제 연관관계로 맺어있는 객체를 사용하는 시점에 DB에 조회하는 쿼리가 발생하여 값을 조회할 수 있다.!! HibernateProxy의 필드를 참고하지 말고, target이 가리키고 있는 객체를 살펴보아야 한다!!

0개의 댓글