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

yjwdbswldn님의 프로필 이미지
yjwdbswldn

작성한 질문수

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

hibernate.default_batch_fetch_size 적용 안되는 경우

해결된 질문

작성

·

262

0

안녕하세요 김영한님 !! 

강의 정말 재미있게 잘 들었습니다 ㅎㅎ 

다름이 아니라 JPA 를 사용하여 프로젝트를 진행하던 와중에 이해가 가지 않는 현상이 있어서 질문드립니다. 

 

default_batch_fetch_size가 설정되어 있고 동일한 메소드로 프록시 객체를 get 할 때 하나는 in 쿼리가 정상동작하고 하나는 동작하지 않는 경우가 있습니다. 

자세한 상황은 여기 블로그 글에 써 놓았는데 너무 길어서 요약해보겠습니다 !! 

 

프로젝트 실제 코드는 너무 길어서 비슷한 상황을 간단한 테스트 코드로 재현해보았습니다. 

- 엔티티 구조 

Member 엔티티의 연관엔티티로 Team이 있습니다. 다대일 관계입니다. 

cascade 옵션은 주지 않았습니다. (Team 은 이미 존재하는 엔티티 이기 때문입니다) 

fetch 타입은 LAZY 입니다

 

1. in 쿼리가 정상 동작하는 테스트코드 

@DisplayName("Member 리스트 조회 시 Team을 lazy loading 할 때 in 쿼리 Team이 한꺼번에 조회된다.")
@Test
void team_inquery_working() {

    // given
    Team teamA = new Team("TeamA");
    Team teamB = new Team("TeamB");
    teamRepository.save(teamA);
    teamRepository.save(teamB);

    testEntityManager.flush();
    testEntityManager.clear();

    Member member1 = new Member("member1");
    Member member2 = new Member("member2");
    member1.setTeam(teamA);
    member2.setTeam(teamB);

    memberRepository.save(member1);
    memberRepository.save(member2);

    testEntityManager.flush();
    testEntityManager.clear();

    // when
    List<Member> members = new ArrayList<>();
    members.add(memberRepository.findById(1L).get()); // Member 를 조회하는 쿼리가 생성된다.
    members.add(memberRepository.findById(2L).get());

    List<String> teamNames = members.stream()
        .map(member -> member.getTeam().getName())
        .collect(toList()); // Team 을 조회하는 쿼리가 in 쿼리로 수행된다.

    // then
    assertThat(teamNames).hasSize(2);
}

 

2. in 쿼리가 동작하지 않는 코드 

@DisplayName("Team을 initialize 할 때 in 쿼리가 수행되지 않는다.")
@Test
void team_inquery_notWorking() {

    EntityManager em = testEntityManager.getEntityManager();

    // given
    Team savedTeamA = teamRepository.save(new Team("TeamA"));
    Team savedTeamB = teamRepository.save(new Team("TeamB"));

    testEntityManager.flush();
    testEntityManager.clear();

    // when
    Member member1 = new Member("member1");
    Member member2 = new Member("member2");

    Team teamA = em.getReference(Team.class, savedTeamA.getId()); // Team은 프록시 객체다.
    Team teamB = em.getReference(Team.class, savedTeamB.getId());
    member1.setTeam(teamA);
    member2.setTeam(teamB);

    memberRepository.save(member1);
    memberRepository.save(member2);

    testEntityManager.flush();

    List<Member> members = new ArrayList<>();
    members.add(memberRepository.findById(1L).get()); // 영속성 컨텍스트에 있는 Member를 로딩한다.
    members.add(memberRepository.findById(2L).get());

    List<String> teamNames = members.stream()
        .map(member -> member.getTeam().getName()) // 각 멤버의 개수만큼 team을 select하는 쿼리를 실행한다.
        .collect(toList());

    // then
    assertThat(teamNames).hasSize(2);
}

 

 

- 위 코드와 아래 코드의 차이는 다음입니다. 

- 위 코드는 Member를 조회할 때 영속성 컨텍스트에 Team의 프록시 객체가 로딩됩니다. 

- 아래 코드는 Member가 저장이 될 때 getReference()를 통해 영속성 컨텍스트로 불러온 Team의 프록시 객체와 연관관계를 맺습니다. 

 

- 위 코드와 아래코드는 

- Member와 Team의 연관관계가 잘 매핑되어 있다는 것 (디버깅 해도 두 경우 모두 Team과 연관관계가 잘 맺어져 있는 것을 확인했습니다)

- Team 이 프록시 객체인 상태로 영속성 컨텍스트에 있다는 것 (하나는 lazy loading, 하나는 getReference 로 가져온 프록시 객체입니다. 이 역시 디버깅하여 확인했습니다)

라는 부분에서 동일한 상황이라고 생각을 하였는데 왜 in 쿼리 적용은 다르게 나갈까요 ? (해결하기 위해서 영속성 컨텍스트를 한번 초기화 clear 해주면 되지만 이유가 궁금합니다 !! )

 

제가 잘못 이해하고 있는 부분이 있는걸까요 ??

감사합니다 😊 !! 

 

답변 3

2

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. yjwdbswldn님

문제가 되는 테스트를 기반으로 설명드리겠습니다.

Team teamA = em.getReference(Team.class, savedTeamA.getId());

Team teamB = em.getReference(Team.class, savedTeamB.getId());

이렇게 코드를 호출하면 영속성 컨텍스트에는 프록시 객체가 존재하게 됩니다.

그리고 이후에 flush()는 호출했지만, clear()는 호출하지 않았습니다.

이렇게 되면 영속성 컨텍스트에 프록시 teamA가 남아있게 됩니다.

 

이후에 member.getTeam()을 조회하게 되면 영속성 컨텍스트에 남아있는 프록시 teamA가 조회됩니다.

따라서 JPA는 이미 영속성 컨텍스트에 있는 객체라 생각하고 쿼리를 실행하지 않습니다.

이후에 각각 쿼리가 실행된 것은 프록시 teamA, teamB를 각각 터치해서 초기화 하기 때문입니다.

 

참고로 JPA는 영속성 컨텍스트에 있는 엔티티의 일관성을 매우 중요하게 생각합니다. 따라서 최초에 한번 프록시가 영속성 컨텍스트로 등록되면 다음에 조회할 때도 프록시가 조회됩니다. 그래야 == 조건이 만족하니까요.

감사합니다.

정말 감사합니다 영한님

1

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. yjwdbswldn님

전체 프로젝트를 압축해서 구글 드라이브로 공유해서 링크를 남겨주세요.

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

https://bit.ly/3fX6ygx

 

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

 

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

1. 실행 방법을 알려주세요.

2. 어떻게 문제를 확인할 수 있는지 자세한 설명을 남겨주세요.

감사합니다.

0

yjwdbswldn님의 프로필 이미지
yjwdbswldn
질문자

감사합니다 !!! 

 

- 프로젝트 링크 

링크

 

- 실행방법

test.java.com.example.jpastudy 패키지 하위에 batchsize 패키지의 BatchSizeTest.java 가 있습니다. 

해당 테스트코드에 in query가 동작하는 경우와 아닌 경우 두개가 구현되어 있습니다 !! 

 

- 문제 확인 방법

1. 첫번째 테스트코드 실행 시 콘솔에 출력되는 가장 하단에 마지막 쿼리에 team 이 in 쿼리로 정상 동작합니다. 

2. 두번째 테스트코드 실행 시 콘솔 가장 하단에 team이 select 되는 쿼리가 각각 2번 출력됩니다.

본래 위 테스트코드처럼 in쿼리로 동작해야한다고 생각했으나 그러지 않았습니다 !! 질문을 한 포인트입니다. 

 

자세히 봐주셔서 감사합니다 !! 

yjwdbswldn님의 프로필 이미지
yjwdbswldn

작성한 질문수

질문하기