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

강용민님의 프로필 이미지
강용민

작성한 질문수

실전! 스프링 데이터 JPA

순수 JPA 페이징과 정렬

Percictence Context에 대해 질문있습니다.

작성

·

340

·

수정됨

0

[질문 템플릿]
1. 강의 내용과 관련된 질문인가요? (예/아니오)
2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예)
3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예)

[질문 내용]
안녕하세요. 현재 JPA에 대해 공부하고 있는 학생입니다. 현재 N+1에 대한 이슈를 테스트하는 중에 Entity의 생명주기에 대한 궁금점이 있어 질문을 남겼습니다.

EntityManager 초기화 시 1차 캐시에 있는 Entity는 자동으로 Flush가 되나요?

N+1 확인을 하기위해 모든 Entity를 저장한 후EntityManager를 초기화 시켜 1차 캐시를 초기화시켰습니다. 이유는 1차 캐시에 원하는 Entity가 존재한다면 해당 Entity를 DB를 거치지 않는다고 알고 있어서입니다.

코드는 다음과 같습니다.

@ExtendWith(SpringExtension.class)
@DataJpaTest
public class StudyRoomRepositoryTest {
  @Autowired private StudyRoomRepository studyRoomRepository;
  @Autowired private UserRepository userRepository;

  @PersistenceContext EntityManager em;

  private List<StudyRoom> studyRooms = new ArrayList<>();
  private List<User> users = new ArrayList<>();

  @BeforeEach
  public void setUp() {
    users.add(userRepository.save(User.builder().userUuid(UUID.randomUUID()).email("tester@gmail.com").password("password").nickName("tester").build()));
    users.add(userRepository.save(User.builder().userUuid(UUID.randomUUID()).email("tester2@gmail.com").password("password").nickName("tester2").build()));
    studyRooms.add(
        studyRoomRepository.save(StudyRoom.builder().roomName("TestStudyRoom1").constructor(users.get(0)).build()));
    studyRooms.add(
        studyRoomRepository.save(StudyRoom.builder().roomName("TestStudyRoom2").constructor(users.get(1)).build()));

  }
  
  @Test
  @DisplayName("users N+1 문제 Test")
  public void UsersNPlusOneTest(){
    em.clear();  //EntityManager 초기화
    List<StudyRoom> findStudyRooms = studyRoomRepository.findAll();
    for(StudyRoom findStudyRoom: findStudyRooms){
      System.out.println(findStudyRoom.getConstructor());
    }
  }
}

예상대로 N+1 이슈에 대해 확인할 수 있었습니다. 하지만 궁금한점이 생겼습니다. 제가 알기로는 Save 메소드가 호출된다 해도 Transaction이 진행중이면 실제 DB에는 안 넘어가는것으로 알고있습니다. 또한 만약 DB로 넘기고 싶다면 Flush를 통해 넘길 수 있는 것으로 알고있습니다. 근데 저는 SaveAndFlush() 메소드가 아닌 Save() 메소드를 호출했음에도 불구하고 Select Query로그가 찍힙니다.

Hibernate: 
    select
        studyroom0_.study_room_id as study_ro1_1_,
        studyroom0_.create_at as create_a2_1_,
        studyroom0_.is_deleted as is_delet3_1_,
        studyroom0_.update_at as update_a4_1_,
        studyroom0_.user_id as user_id7_1_,
        studyroom0_.room_name as room_nam5_1_,
        studyroom0_.room_uuid as room_uui6_1_ 
    from
        study_rooms studyroom0_
Hibernate: 
    select
        user0_.user_id as user_id1_2_0_,
        user0_.create_at as create_a2_2_0_,
        user0_.is_deleted as is_delet3_2_0_,
        user0_.update_at as update_a4_2_0_,
        user0_.email as email5_2_0_,
        user0_.nick_name as nick_nam6_2_0_,
        user0_.password as password7_2_0_,
        user0_.role as role8_2_0_,
        user0_.user_uuid as user_uui9_2_0_ 
    from
        users user0_ 
    where
        user0_.user_id=?
com.twoleader.backend.domain.user.entity.User@522fb69

이유를 잘 모르겠습니다. 제 예상으로는 EntityManager의 clear()메소드 호출 시 1차 캐시 안에 Entity들이 존재한다면 자동으로 Flush()메소드가 호출되는 것으로 추측이 되는데 맞는지 여쭙고 싶습니다. 만약 아니라면 해당 데이터는 어디서 불러온것인지 궁금합니다.

답변 2

0

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

안녕하세요. 강용민님

JPA 기본편 강의에서 설명을 드리는 내용이지만, 간략하게 답변을 드리겠습니다.

JPA에서 플러시 되는 조건에는 트랜잭션 커밋도 있지만, JPQL을 실행했을 때도 있습니다.

그러니까 findAll을 사용하게 되면 JPQL이 실행되고, 결과적으로 플러시가 일어나서 DB에 결과가 반영됩니다.

JPA에서 데이터를 조회할 때는 2가지 방법이 있습니다.

1. em.find()와 같이 엔티티 하나를 조회하는 방법

2. JPQL을 사용해서 여러 엔티티를 조회하는 방법

JPA에서 em.find()와 같은 엔티티 하나를 조회할 때는 영속성 컨텍스트를 먼저 찾습니다. 그리고 영속성 컨텍스트에서 조회가 되지 않을 때는 DB에서 조회합니다.

반면에 JPQL을 사용하게 되면 먼저 데이터베이스에 SQL을 실행해서 그 결과를 가져온 다음에 해당 결과를 영속성 컨텍스트에 보관합니다. 왜냐하면 영속성 컨텍스트에 없는 데이터가 DB에 있을 수도 있기 때문에 DB에서 조회는 필수 입니다.

JPA는 기본기가 정말 중요하기 때문에 JPA를 기본편을 꼭 학습하시는 것을 권장드립니다.

감사합니다.

0

안녕하세요, 인프런 AI 인턴이에요.

EntityManager를 초기화할 때 1차 캐시에 있는 Entity는 자동으로 Flush되지 않아요. EntityManager를 초기화하면 기존에 1차 캐시에 있던 데이터는 모두 clear되기 때문에 DB에 다시 저장해야하는 경우가 있어요.

N+1을 확인하기 위해 모든 Entity를 저장한 후 EntityManager를 초기화시켜 1차 캐시를 초기화시켰다고 하셨는데, 이렇게 하면 원하는 Entity는 DB를 거치지 않고 1차 캐시에서 가져오게 됩니다.

그러나 EntityManager를 초기화하면 1차 캐시는 초기화되지만, DB에 다시 액세스해서 Entity를 가져와야하기 때문에 Flush가 발생하게 됩니다. 그래서 성능상의 이슈가 발생할 수 있으니 주의하셔야 해요.

만약 N+1 문제를 확인하고자 한다면 fetch join이나 entity graph를 사용하여 연관된 엔티티도 함께 가져오는 방법을 고려해보세요. 이 방법을 사용하면 DB에 다시 액세스해서 엔티티들을 가져오는 횟수를 최소화할 수 있어요.

참고로, 질문에 게시된 코드가 잘리는 부분이 있어 전체 코드를 확인하지 못했습니다. 필요한 코드가 있다면 추가로 질문해주세요. 요청하신 답변이 도움이 되었는지 궁금하네요. 추가로 궁금한 점이 있으시다면 언제든지 물어보세요!

강용민님의 프로필 이미지
강용민

작성한 질문수

질문하기