• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    해결됨

일대일 양방향 연관관계에서 N+1 문제

24.03.19 23:48 작성 24.03.19 23:49 수정 조회수 158

0

엔티티 연관 관계

Recruitment <(1)---(1)> Study

 

엔티티 코드

@Entity
public class Recruitment {

	@Id
	@GeneratedValue
	@Column(name = "recruitment_id")
	private Long id;

	@OneToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "id")
	private Study study;
}
@Entity
public class Study {

	@Id
	@GeneratedValue
	@Column(name = "study_id")
	private Long id;

	@OneToOne(mappedBy = "study", fetch = FetchType.LAZY)
	private Recruitment recruitment;
}

 

 

질문

Recruitment 를 조회하고 Study 프록시 객체에 접근할 때, Recruitment 조회 쿼리가 한 번 더 나가는 이유를 모르겠습니다. (자세한 내용은 아래 코드의 주석으로 추가했습니다.)

 

테스트 코드


@Test
@Transactional
void test() {
    Study study = new Study();
    study.setTitle("스터디A");
    studyRepository.save(study);

    Recruitment recruitment = new Recruitment();
    recruitment.setStudy(study);
    recruitment.setTitle("스터디A의 모집공고");
    recruitmentRepository.save(recruitment);

    entityManager.flush();
    entityManager.clear();

    // ---------------------//
    Recruitment findRecruitment = recruitmentRepository.findById(1L).get();	// Recruitment 조회
    System.out.println("findRecruitment.getStudy() start");
    Study findStudy = findRecruitment.getStudy();
    System.out.println("findRecruitment.getStudy() end");

    System.out.println("findStudy.getTitle() start");
    String title = findStudy.getTitle();	// Study 지연로딩만 나갈 것으로 예상했지만, Recruitment 조회도 발생함
    System.out.println("findStudy.getTitle() end");
}

SQL 로그

select
    r1_0.recruitment_id,
    r1_0.id,
    r1_0.title 
from
    recruitment r1_0 
where
    r1_0.recruitment_id=?

// "findStudy.getTitle() start" (Study 지연 로딩)
select
    s1_0.study_id,
    s1_0.title 
from
    study s1_0 
where
    s1_0.study_id=?

select  // Recruitment 조회 로직이 왜 또 나가는가?
    r1_0.recruitment_id,
    r1_0.id,
    r1_0.title 
from
    recruitment r1_0 
where
    r1_0.id=?
// "findStudy.getTitle() end"

답변 1

답변을 작성해보세요.

2

안녕하세요. 김준기님

OneToOne 매핑의 경우 지연 로딩 설정에 한계가 있습니다.

자세한 내용은 다음 내용을 읽어보시면 도움이 되실거에요.

https://www.inflearn.com/questions/40670

추가로 해당 글을 읽어보시고 다음 코드를 실행해보시면 한계가 이해가 되실거에요.

package org.example.onetoone;

import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest
class RecruitmentTest2 {

    @Autowired
    EntityManager em;

    @Autowired
    RecruitmentRepository recruitmentRepository;

    @Test
    @Transactional
    void test() {
        Study study = new Study();
        study.setTitle("스터디A");
        em.persist(study);

        Recruitment recruitment = new Recruitment();
        recruitment.setStudy(study);
        recruitment.setTitle("스터디A의 모집공고");
        em.persist(recruitment);

        em.flush();
        em.clear();

        System.out.println("findStudy start");
        // Study만 조회될 것으로 예상했지만, Recruitment 조회도 발생함
        Study findStudy = em.find(Study.class, study.getId());
        System.out.println("findStudy end");
    }

}

실행 결과

findStudy start
Hibernate: select s1_0.study_id,s1_0.title from study s1_0 where s1_0.study_id=?
Hibernate: select r1_0.recruitment_id,r1_0.study_id,r1_0.title from recruitment r1_0 where r1_0.study_id=?
findStudy end

실행 결과를 보면 Study만 조회될 것으로 예상했지만, LAZY로 설정한 Recuritment도 함께 조회됩니다.

감사합니다.

김준기님의 프로필

김준기

질문자

2024.03.21

안녕하세요 영한님. 출시한지 꽤 오래된 강의임에도, 상세하게 답변 남겨주셔서 감사합니다.
첨부해주신 내용을 토대로 두 가지 궁금한 점이 남아, 영한님의 의견을 꼭 여쭙고 싶어 질문 드려요 🙂

 


 

질문1. OneToOne 양방향 연관 관계에서, 주인이 아닌 엔티티 를 조회할 때, 주인 엔티티추가조회하는 이유


image

저는 데이터베이스의 메커니즘과 객체지향의 메커니즘의 차이라고 생각했습니다.
case1: Recruitment 엔티티 조회
Recruitment 테이블 한 번의 조회만으로 Recruitment와 Recruitment가 참조하고 있는 Study를 나타낼 수 있어요. Recruitment 테이블이 Study 테이블을 외래키로 "참조"하고 있기 때문이에요.

case2: Study 엔티티 조회
Study 테이블 한 번의 조회만으로는 Study가 참조하고 있는 Recruitment를 나타낼 방법이 없어요. Study 테이블엔 Recruitment 테이블과 관련한 어떠한 정보도 없기 때문이에요.

Study가 참조하고 있는 Recruitment가 정말로 없는 것인지 (:= Null), 존재는 하는 것인지 (:= Proxy) 객체로 표현해야 하는데, 이를 알 방법이 없으므로 Recruitment 조회 쿼리가 한 번 더 발생하는 것이라 생각했습니다.

개인적인 고민을 통해 결론을 도출하였는데, 올바른 결론인지 여쭤보고 싶어요 🤔🤔🤔

 

 


질문2. 해당 문제에 대한 실무 관점에서의 현실적 해결방안

(공유 주신 링크를 통해 구조적인 변경을 통한 해결, 구조를 유지하고 쿼리 최적화로 해결 방법에 대해 인지하게 됐습니다. 감사합니다.)

구조적 변경을 통한 해결 방법은 아래의 이유로 실무에서 적용하기 어렵지 않을까? 라는 생각이 들었습니다.
1) 일대다, 다대일 연관 관계로 변경
해당 방법은 비즈니스 요구사항 변경될 가능성이 큼 을 내포한다는 생각이 들었습니다. 그에 따라 기획/ 디자인/프론트/DBA 분들의 변경 사항에 대한 코스트가 높을 것으로 판단되는데, 실무에서도 해당 방법을 고려하는지 여쭙고 싶습니다 🙂

2) FK 위치 변경
상용 중인 서비스에서는 물리적 데이터 구조를 변경하기 까다롭다 판단되어, 해당 방법에 대해서도 동일한 질문을 여쭙고 싶습니다 🙂

위와 같은 이유로 아래의 두가지 해결 방법을 실무에서 주로 고려하게 될 것 같은데, 아직 실무 경험이 없어서 궁금하여 질문 드립니다..!

  1. 서비스에서 발생가능한 조회 로직을 고려하고, 이를 바탕으로 양방향 연관 관계 끊기 여부 결정
    2. fetch join + batch size 로 성능 최적화

 

안녕하세요. 김준기님

질문1

생각하신 내용이 맞습니다.

질문2

이 부분은 케이스가 너무 다양한데요.

일반적인 경우에는 크게 문제가 되지 않고, 성능 이슈가 있는 경우에 fetch join, batch size로 대부분 해결이 가능합니다 :)

감사합니다.

김준기님의 프로필

김준기

질문자

2024.03.22

답변 감사합니다 :)
좋은 저녁 되세요 🙂🙂