JPA

Spring Data JPA Bulk Update & Entity Graph

빈코 2022. 10. 14. 15:21

지난 포스팅에서는 Spring Data JPA를 사용해서 페이징 처리를 손쉽게 구현했어요. 오늘은 벌크 업데이트엔티티 그래프에 대해 알아보려 합니다.

로고
Bulk Update & Entity Graph

개요

Bulk Update는 클라이언트의 요구 조건에 맞는 데이터들을 한번에 변경하는 것을 의미합니다. JPA에서는 변경 감지(더티 체킹)를 해서 데이터를 업데이트하기 때문에, 한 번에 수정하기 위한 쿼리가 필요하고 그 쿼리를 벌크 수정이라고 불러요.

 

Entity Graph연관된 엔티티들을 SQL 한번에 조회하는 방법입니다. 사실상 fetch join의 간편 버전이라고 할 수 있어요. 그럼 한번 살펴볼까요?

Bulk Update📗

Bulk update의 방법으로는 순수 JPA를 사용하는 방법과 Spring Data JPA의 @Query를 사용하는 방법으로 나뉩니다. 먼저 순수 JPA를 사용해볼게요. 비즈니스 로직은 넘겨받은 나이보다 같거나 많은 멤버의 나이를 1씩 증가시키는 것으로 하겠습니다. 

 

@Repository
public class MemberJpaRepository {
  public int bulkAgePlus(int age) {
      int resultCount = em.createQuery("update Member m set m.age = m.age + 1 where m.age >= :age")
            .setParameter("age", age)
            .executeUpdate();

      return resultCount;
  }
}

 

사실 쿼리문은 간단하기 때문에 따로 부연설명은 필요없을 것 같고, update 문에서는 executeUpdate를 사용해야 한다는 점만 알아두시면 될 것 같아요. 다음 테스트 코드는

 

@Test
@Transactional
public void bulkUpdate() throws Exception {
    //given
    memberJpaRepository.save(new Member("member1", 10));
    memberJpaRepository.save(new Member("member2", 19));
    memberJpaRepository.save(new Member("member3", 20));
    memberJpaRepository.save(new Member("member4", 21));
    memberJpaRepository.save(new Member("member5", 40));

    //when
    int resultCount = memberJpaRepository.bulkAgePlus(20);

    //then
    assertThat(resultCount).isEqualTo(3);
}

when 단계에서 bulkAgePlus 함수에 인자값으로 20을 넘기기 때문에, given 단계에서의 데이터 중에 나이가 20 이상인 멤버들이 3명이어서, then 단계에서 resultCount가 3인 것을 확인할 수 있었어요.

 

한 가지 주의해야 할 점은 DML 쿼리이기 때문에 반드시 @Transactional 에너테이션을 사용해야 합니다. 수행시 로그를 확인해보면

 

로그
로그

 

5명의 맴버를 Insert 한 후, 한 번에 Update를 하는 것을 확인할 수 있습니다. 하지만 주의사항이 있어요. 벌크 업데이트는 영속성 컨텍스트와 관계없이 작동하기 때문에 바로 DB에 적용이 됩니다. 이 말은 즉슨, 위에 테스트 코드대로 업데이트한 후 member3의 나이를 member3.getAge()를 한다면 변경되지 않은 20살로 가져오게 됩니다.

 

📌 벌크성 수정 쿼리를 진행할 때 DB에 바로 적용되기 때문에 영속성 컨텍스트를 비워주는 코드도 함께 들어가야 한다. 그렇지 않으면 영속성 컨텍스트에는 값이 그대로고 DB값만 바뀌어 충돌이 일어난다.

 

@Test
@Transactional
public void bulkUpdate() throws Exception {
    //given
    memberJpaRepository.save(new Member("member1", 10));
    memberJpaRepository.save(new Member("member2", 19));
    memberJpaRepository.save(new Member("member3", 20));
    memberJpaRepository.save(new Member("member4", 21));
    memberJpaRepository.save(new Member("member5", 40));

    //when
    int resultCount = memberJpaRepository.bulkAgePlus(20);
    
    em.flush();
    em.clear();
    
    Member member3 = memberJpaRepository.findByUsername("member3").get(0);

    //then
    assertThat(resultCount).isEqualTo(3);
    assertThat(member3.getAge()).isEqualTo(21);
}

수정한다면 위의 코드처럼 됩니다. em.flush()로 DB에 바로 적용한 후에, em.clear()로 영속성 컨텍스트를 비워줍니다.


Spring Data JPA에서는 어떻게 구현할까요? 사실 에노테이션을 사용한다는 것 말고는 크게 다른 점은 없습니다. 유지보수 측면에서 조금 더 좋은 것 같네요!

 

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Modifying(clearAutomatically = true)
    @Query("update Member m set m.age = m.age + 1 where m.age >= :age")
    int bulkAgePlus(@Param("age") int age);
}

주의할 점은 @Modifying 입니다. JPA 기본 동작이 select - update로 이루어져 있기 때문에 바로 update만 하겠다고 알려주는 에너테이션이 @Modifyting인데, 이 에너테이션을 사용할 때는 반드시 반환 타입을 void, int, Integer로 지정해야 합니다. 다른 반환 타입은 바로 오류 나요^^..

 

옵션으로는 clearAutomatically가 있는데 true로 설정하면 영속성 컨텍스트를 비워주는 역할을 해줍니다. 하지만 사실상 비즈니스 로직마다 상황이 다르기 때문에 항상 true로 하는 것은 옳은 방법은 아닌 것 같아요. 각자 상황에 맞게 구현하시면 될 것 같습니다.

 

테스토 코드는 순수 JPA 테스트 코드와 동일합니다.

 

Entity Graph📒

 

관계
관계

이전 포스팅을 보셨다면 아시겠지만, 도메인 모델을 보면 Member와 Team은 지연로딩 관계(다대일 관계)로 설정했었습니다. 따라서 Member와 같이 Team의 데이터를 조회할 때마다 쿼리가 실행되는 N+1문제가 발생합니다.

 

📌 지연 로딩을 모르신다면 꼭 링크를 클릭해서 선수 학습해주세요. 정말 중요합니다!

 

@Test
@Transactional
    public void findMemberLazy() throws Exception {
        //given
        //member1 -> teamA
        //member2 -> teamB

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        teamRepository.save(teamA);
        teamRepository.save(teamB);
        memberRepository.save(new Member("member1", 10, teamA));
        memberRepository.save(new Member("member2", 20, teamB));
        em.flush(); // DB에 바로 반영
        em.clear(); // 영속성 컨텍스트 Clear

        //when
        List<Member> members = memberRepository.findAll();

        //then
        for (Member member : members) {
            member.getTeam().getName();
        }
    }

 

테스트 코드를 보시면 when 단계에서는 given 단계에서 저장한 맴버들을 가져옵니다. 이때, 멤버들의 팀은 가짜 데이터로 가져오기 때문에 쿼리가 한 번만 날아갑니다. (memberRepository.findAll 메서드는 패치 조인한 것이 아닌 JPA가 기본 제공하는 함수)

 

하지만 문제는 then 단계입니다. 성능 최적화를 위해 각각의 멤버의 팀은 가짜 데이터로 가져왔었는데, 직접적으로 member.getTeam().getName()을 호출하면 이때 Lazy 로딩이 실행되어 해당 멤버의 팀 이름을 가져오는 쿼리가 나가게 됩니다. 

 

결론적으로 맴버의 수(2) + 멤버 가져오는 쿼리(1) = 총 3개의 쿼리가 나갑니다. 지금은 데이터가 적지만 만약 멤버가 만 명이면 10,001개의 쿼리가 나가는 것입니다. 

 

해결방법으로는 fetch join을 해서 한번에 데이터를 가져오는 것입니다.

 

public interface MemberRepository extends JpaRepository<Member, Long>{
   @Query("select m from Member m left join fetch m.team")
   List<Member> findMemberFetchJoin();
}

 

위 테스트 코드에서 findAll() 메서드 대신 findMemberFetchJoin() 메서드로 바꿔주면 N + 1 문제가 해결됩니다.


Spring Data JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하게 도와줍니다. 이 기능을 사용하면 JPQL 없이 페치 조인을 사용할 수 있습니다.

 

public interface MemberRepository extends JpaRepository<Member, Long>{
    @Override
    @EntityGraph(attributePaths = {"team"})
    List<Member> findAll();
}

@EntityGraph 에너테이션을 사용하는 것이 직접 쿼리를 작성하는 것보다 훨씬 더 편리하겠죠?

 

이 밖에도 JPQL + 엔티티 그래프를 같이 사용할 수도 있고, 이름으로 구현하는 메서드에도 사용할 수 있습니다.

 

public interface MemberRepository extends JpaRepository<Member, Long>{
    @Override
    @EntityGraph(attributePaths = {"team"})
    List<Member> findAll();
    
    //JPQL + 엔티티 그래프
    @EntityGraph(attributePaths = {"team"})
    @Query("select m from Member m")
    List<Member> findMemberEntityGraph();

    //메서드 이름으로 쿼리에서 특히 편리하다.
    @EntityGraph(attributePaths = {"team"})
    List<Member> findEntityGraphByUsername(@Param("username") String username);
}

 

 


👨‍👩‍👦‍👦 오픈채팅방 운영

취업을 준비하는 예비 개발자분들을 위한 질문&답변할 수 있는 공간을 만들었습니다. 취업과 이직을 하기 위해서 어떤 걸 중점적으로 준비해야 하는지부터 포트폴리오&이력서 작성법 등 다양한 질문들을 받고 답변을 드립니다. 참여하셔서 다양한 정보 얻고 가시면 좋을 것 같네요😁

 

참여코드 : 456456

https://open.kakao.com/o/gVHZP8dg

 

비전공 개발자 취업 준비방(질문&답변)

#비전공 #개발자 #취업 #멘토링 #부트캠프 #국비지원 #백엔드 #프론트엔드 #중소기업 #중견기업 #자바 #Java #sql

open.kakao.com

 


👨‍💻 전자책 출간

아울러 제가  🌟비전공자에서 2년만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간 하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해주세요😁

https://kmong.com/gig/480954

 

비전공개발자 2년만에 중견기업 들어간 방법 | 14000원부터 시작 가능한 총 평점 0점의 전자책, 취

0개 총 작업 개수 완료한 총 평점 0점인 Binco의 전자책, 취업·이직 전자책 서비스를 0개의 리뷰와 함께 확인해 보세요. 전자책, 취업·이직 전자책 제공 등 14000원부터 시작 가능한 서비스

kmong.com


 

마치며

지금까지 Spring Data JPA의 Bulk Update와 Entity Graph를 알아보았습니다. 이 포스팅을 마지막으로 Spring Data JPA 시리즈는 끝이 날 것 같네요. 기존 JPA를 모른다면 Spring Data JPA가 제공하는 많은 기능들의 동작 방식을 잘 몰랐을 것 같아요. 더 공부해서 더 좋은 글로 찾아뵐게요😄

 

Spring Data JPA Series

1. Spring Data JPA와 기존 JPA의 차이점 및 사용법

2. Spring Data JPA 쿼리 메서드

3. Spring Data JPA 페이징 처리

4. Spring Data JPA Bulk Update & Entity Graph

Reference

실전! 스프링 데이터 JPA - 김영한

반응형