[JPA] @OneToOne 양방향 맵핑 지연 로딩 미적용 문제 개선

김상현·2022년 12월 29일
0

JPA

목록 보기
3/3
post-thumbnail

양방향 맵핑 지연 로딩 미적용 문제 개선


0. 발생한 문제


상품 목록 서비스는 사용자에게 쇼핑몰에 등록된 모든 상품(Item) 정보를 페이지 단위로 제공하는 서비스이다.
해당 서비스는 한번의 select 쿼리를 통해 상품에 대한 데이터를 제공 했었다.

문제가 발생한 시점은 최근 쇼핑몰에 장바구니 서비스를 구현한 이후부터였다.

🧷 CartItem Entity 구성도

위 구성도는 상품(Item)과 장바구니 상품(CartItem)의 엔티티 관계를 표현한 테이블이다.
구성도를 보면 장바구니 상품(CartItem)과 상품(Item)은 일대일(@OneToOne) 관계를 맺고 있다.

🧷 중복 select 쿼리

select
  item0_.item_id as item_id2_4_,
  item0_.created_date as created_3_4_,
  item0_.last_modified as last_mod4_4_,
  item0_.dtype as dtype1_4_,
  item0_.member_id as member_14_4_,
  item0_.name as name5_4_,
  item0_.price as price6_4_,
  item0_.stock_quantity as stock_qu7_4_,
  item0_.artist as artist8_4_,
  item0_.etc as etc9_4_,
  item0_.author as author10_4_,
  item0_.isbn as isbn11_4_,
  item0_.actor as actor12_4_,
  item0_.director as directo13_4_
from
  item item0_
order by
  item0_.created_date desc limit ?

select
  cartitem0_.cart_item_id as cart_ite1_0_0_,
  cartitem0_.created_date as created_2_0_0_,
  cartitem0_.last_modified as last_mod3_0_0_,
  cartitem0_.count as count4_0_0_,
  cartitem0_.item_id as item_id5_0_0_,
  cartitem0_.member_id as member_i6_0_0_
from
  cart_item cartitem0_
where
  cartitem0_.item_id=?
.
.
.
select
  cartitem0_.cart_item_id as cart_ite1_0_0_,
  cartitem0_.created_date as created_2_0_0_,
  cartitem0_.last_modified as last_mod3_0_0_,
  cartitem0_.count as count4_0_0_,
  cartitem0_.item_id as item_id5_0_0_,
  cartitem0_.member_id as member_i6_0_0_
from
  cart_item cartitem0_
where
  cartitem0_.item_id=?

SQL 문을 보면 알 수 있듯이 상품(Item) 데이터를 불러오기 위한 select 쿼리가 발생할 때 상품과 연관 관계를 맺고 있는 모든 CartItem 객체를 단건으로 호출하는 N + 1 문제가 발생했다.


1. 문제 해결 과정


📌 첫 번째로 확인한 부분은 각 엔티티에 부여된 fetch() 속성을 확인했다. 각 엔티티에 지연 로딩(LAZY)을 적용하지 않으면 상품을 호출할 때 마다 즉시 로딩(EAGER) 방식으로 쿼리를 발생시킬 수 있기 때문이다.

🧷 Item Entity

@OneToOne(fetch = LAZY, mappedBy = "item")
protected CartItem cartItem;

🧷 CartItem Entity

@OneToOne(fetch = LAZY)
@JoinColumn(name = "item_id")
private Item item;

아쉽게도 각 엔티티 fetch() 에 적용된 속성은 지연 로딩(LAZY)이었다. 즉시 로딩(EAGER) 방식으로 발생한 문제가 아님을 확인할 수 있었다.

📌 두 번째로 확인한 부분은 Controller, Service, Repository 였다.
비지니스 로직 중에서 프록시 상태의 장바구니 상품(CartItem)을 실제 호출해서 DB에서 해당 데이터를 호출하는 메서드가 있는지 확인하였다. 코드를 역으로 따라올라가면서 확인해 보았지만 결국 찾을 수 없었다.

📌 마지막으로 확인한 부분은 DTO, HTML 였다.
엔티티를 DTO로 변환하는 과정에서 프록시 상태인 장바구니 상품(CartItem)을 호출하는 상황이 존재하는지 확인했지만 찾을 수 없었다.
현재 프로젝트는 Thymeleaf SSR을 사용하고 있다. 만약 HTML에서 th 속성을 통해 프록시 상태인 장바구니 상품(CartItem)을 호출하는 상황이 존재하는지 확인했지만 마찬가지로 찾을 수 없었다.


2. 해결 방법


📒 문제 해결에 도움을 준 고마운 블로그 : OneToOne 관계는 과연 지연로딩이 되는가?

결론부터 말하면 @OneToOne 양방향 매핑은 지연 로딩(LAZY)을 설정해도 즉시 로딩(EAGER)이 적용된다.

@OneToOne 양방향 매핑은 지연 로딩(LAZY)이 적용되지 않는 이유는 아래와 같다.

지연 로딩(LAZY)을 사용하기 위해서 JPA 구현체는 연관 관계 엔티티에 null 혹은 프록시를 만들어 주어야 한다.
만약 연관 관계 엔티티가 null 이라면 해당 엔티티는 존재하지 않다는 것을 의미하고,
연관 관계 엔티티가 프록시라면 해당 엔티티는 존재한다는 것을 의미한다.

null 값이 가능한 @OneToOne 의 경우 프록시 객체로 감쌀 수 없다. 만약 null 값이 가능한 @OneToOne 에 프록시 객체를 넣는다면, 이미 그 순간 결코 null 이 아닌 프록시 객체를 리턴하는 상태가 돼 버리기 때문이다.

따라서 JPA 구현체는 기본적으로 @One-To-One 관계에 지연 로딩(Lazy) 를 허용하지 않고, 즉시 값을 읽어 들인다.

결국 문제를 해결하기 위해서는 (1) 양방향 매핑을 단방향 매핑으로 변경하거나 (2) @OneToOne 관계를 @OneToMany @ManyToOne 관계로 변경해주어야 한다.

엔티티와 비지니스 로직을 확인한 후 상품(Item)에서 장바구니 상품(cartItem)으로 진행되는 메서드가 없음을 확인하고 장바구니 상품(cartItem)이 상품(Item) 객체를 갖는 단방향 매핑으로 변경해주었다.

이후 테스트를 통해 확인한 결과 불필요하게 발생하던 중복 쿼리를 개선할 수 있었다.

🧷 개선된 select 쿼리

select
  item0_.item_id as item_id2_4_,
  item0_.created_date as created_3_4_,
  item0_.last_modified as last_mod4_4_,
  item0_.dtype as dtype1_4_,
  item0_.member_id as member_14_4_,
  item0_.name as name5_4_,
  item0_.price as price6_4_,
  item0_.stock_quantity as stock_qu7_4_,
  item0_.artist as artist8_4_,
  item0_.etc as etc9_4_,
  item0_.author as author10_4_,
  item0_.isbn as isbn11_4_,
  item0_.actor as actor12_4_,
  item0_.director as directo13_4_
from
  item item0_
order by
  item0_.created_date desc limit ?

3. 고찰


얄팍한 지식의 깊이 때문에 편협한 시야를 가지고 문제해결에 접근했던 것이 더 많은 시간을 잡아 먹은 것 같다. 프록시에 대해 조금 알고 있다고 프록시가 호출된 지점만 주구장창 찾았지만, 결국 문제는 엔티티 연관 관계에서 발생한 문제였다. 전문가들은 자신의 의견이 틀릴 수 있음을 공시하지만, 책 한권 읽은 비전문가는 자신의 의견이 틀릴 일이 없다고 우긴다는 장난스러운 농담에 뼈가 있다는 것을 알게된 것 같다. 내가 고민하는 문제를 과거에 미리 고민하고 최적의 결과를 도출하기 위해 노력해준 선배 개발자들에게 항상 감사함을 느낀다.

profile
목적 있는 글쓰기

0개의 댓글