• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    미해결

2depth의 @OneToOne, @ManyToOne N+1문제

24.02.12 15:59 작성 조회수 124

0

Shop, Business, User라는 세 엔티티에 연관관계가 각각 맺어져 있습니다.

  • Shop - Business(연관관계 주인은 Business, OneToOne관계)

  • Business - User(연관관계 주인은 Business, ManyToOne-OneToMany 관계)

여기서, 모든 관계는 Lazy로딩으로 설정되어 있고, shop의 business는 null로 존재하는 상황입니다.

문제의 상황은 크게 두가지인데요.

  • Shop을 조회할 때 Business Lazy로딩으로 설정되었지만, Business에 대한 조회 쿼리가 발생

  • 위 상황에서 Business가 조회되고, 거기에 ManyToOne으로 연관된 User도 조회 쿼리 추가로 발생

그래서 1개의 조회를 했는데, 2개가 추가로 나옵니다.

사실 첫 번째 문제는 찾아보니 null값을 Proxy객체가 담을 수 없어서 조회쿼리가 발생하는 것이라고 들었습니다. 그런데 두 번째 쿼리(User의 조회 쿼리)는 왜 발생하는지 도저히 모르겠네요..ㅠ

아래는 Shop, Business, User의 코드입니다. 코틀린으로 작성된 점 양해부탁드립니다.

@Entity
class Shop(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "shop_id")
    var id: Long = 0L,

...

    @OneToOne(mappedBy = "shop", cascade = [CascadeType.ALL], orphanRemoval = true)
    var business: Business? = null,

)
@Entity
class Business (

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "business_id")
    var id: Long = 0L,

...

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "shop_id")
    var shop: Shop,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    val seller: User
)
@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    var id: Long = 0L,

...

    @OneToMany(mappedBy = "seller", cascade = [CascadeType.PERSIST], orphanRemoval = true)
    val businessList: MutableList<Business> = mutableListOf(),

    )

 

답변 1

답변을 작성해보세요.

0

안녕하세요. 789456jang님

도움을 드리고 싶지만 질문 내용만으로는 답변을 드리기 어렵습니다.

문제가 되는 상황을, 실제 작동하는 코드를 자바로 다시 작성한 다음에 전체 프로젝트를 압축해서 구글 드라이브로 공유해서 링크를 남겨주세요. 꼭 자바로 만들어주세요.

구글 드라이브 업로드 방법은 다음을 참고해주세요.

https://bit.ly/3fX6ygx

주의: 업로드시 링크에 있는 권한 문제 꼭 확인해주세요

 

추가로 다음 내용도 코멘트 부탁드립니다.

1. 문제 영역을 실행할 수 있는 방법

2. 문제가 어떻게 나타나는지에 대한 상세한 설명

 

링크: 공식 서포터즈

링크: 자주하는 질문

감사합니다.

789456jang님의 프로필

789456jang

질문자

2024.02.16

https://drive.google.com/file/d/1S0t6ITx75yiXIO0MTGeYE8Q_M46vmqqa/view?usp=sharing
안녕하세요 영한님, 제가 최대한 자바 코드로 같은 환경을 만들어서 간단하게 구현해보려고 했으나, 자바 코드로 작성 시에 그 문제가 제대로 발생하지 않았습니다. 그래서 차선책으로, 문제 상황을 자세하게 풀어보려고 합니다. (자바로 작성된 코드는 첨부해두었습니다!) 모든 연관관계와 옵션들은 기존 코틀린에서 프로젝트와 일치합니다.

 

1. 문제 영역을 실행할 수 있는 방법

  • UserController에 createUser를 통해 user를 생성한다.

  • ShopController에 createShop을 통해 business와 shop을 생성한다.

  • findShop을 통해 shop을 조회한다.

2. 문제가 어떻게 나타나는지

  • shop을 jpql을 통해 조회했을 때, 쿼리가 'shop을 조회하는 쿼리', 'business를 조회하는 쿼리' 이렇게 두 방이 나갑니다.

     

    image

    • shop을 조회할 때는 jpql을 사용해 가져옵니다. jpql은 DB에 직접적으로 쿼리를 날려 가져오기 때문에 @OneToOne으로 설정되어 있는 shop과 business는 fetch 타입을 Lazy로 설정해두어도 business를 다시 조회하여 가져온다고 잘 알고 있습니다!

    • 그리고 user는 Lazy로 설정되어 있기 때문에 business를 가져오더라도 추가로 쿼리가 발생하지 않는 것으로 예상했습니다. 자바로 작성한 간단한 예제에서는 예상대로 동작하더군요.

  • !!여기서부터 기존 환경(코틀린)에서의 쿼리와 다르게 동작합니다.!!

    • 조회해온 business는 user와 @ManyToOne 관계입니다. 물론 fetch 타입도 Lazy로 설정되어 위에서 보신 두 방의 쿼리가 전부입니다.

    • 하지만 기존 환경에서는 총 세 방의 쿼리가 나가며 user까지 조회가 되는 모습을 볼 수 있었습니다.. 혹시나 해서 business에서 user의 정보를 가져오는 로직이 있는지 확인을 해봤지만 전혀 없었습니다. (아래는 세 방의 쿼리가 나가는 모습입니다.)
      imageimage

  • 너무 풀리지 않는 궁금증이라 breakpoint를 하나하나 찍어보며 봤는데도 실마리를 찾지 못하겠더라구요.. 이게 단서가 될지는 모르겠으나, shop과 business가 각각 조회된 후, DefaultLoadEventListener의 doOnLoad 메서드 파라미터 중 event객체가 User로 받아와졌습니다. (아래 캡처 참조) 따로 user를 호출한 게 아닌, shop을 조회하면서 찍힌 breakpoint입니다.

image

결정적으로 궁금한 것은 Lazy로 설정되어있음에도 불구하고, 특정한 상황에 따라 즉시 조회될 수 있는지가 궁금합니다. 만약에 이런 일이 일어날 수 없다면, 정말 제가 찾지 못하는 어디선가 조회가 되었을 것으로 결론 지으려고 합니다.

감사합니다. 혹시라도 추가적으로 답변에 필요한 부분이 있다면 말씀해주시면 감사하겠습니다 :)

안녕하세요. 789456jang님

저도 정확한 문제는 알 수 없는데요. 자바로 이슈가 없었다고 하신걸 보면 코틀린 때문일 수 있습니다.

코틀린 JPA 이슈로 검색해보시면 도움이 되실거에요.

감사합니다.