인프런 커뮤니티 질문&답변

789456jang님의 프로필 이미지
789456jang

작성한 질문수

자바 ORM 표준 JPA 프로그래밍 - 기본편

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

작성

·

240

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
질문자

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 이슈로 검색해보시면 도움이 되실거에요.

감사합니다.

789456jang님의 프로필 이미지
789456jang

작성한 질문수

질문하기