프로젝트 진행시 연관관계로 맺어있는 객체를 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의 정보를 가지고 있었다. 아래의 그림을 보면 더더욱 이해가 잘 될 것 같다. 저의 엔티티로 해당 그림을 해석하면,
따라서 지연로딩을 사용하면 실제 연관관계로 맺어있는 객체를 사용하는 시점에 DB에 조회하는 쿼리가 발생하여 값을 조회할 수 있다.!! HibernateProxy의 필드를 참고하지 말고, target이 가리키고 있는 객체를 살펴보아야 한다!!