ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Data JPA] 벌크 연산 후 영속성 컨텍스트를 초기화 해야 하는 이유
    Spring Framework 2024. 7. 9. 22:59

    벌크 연산이란?

    벌크 연산은 대량의 데이터를 한번에 수정하거나 삭제하는 연산을 뜻한다. 벌크 연산을 사용하면 일괄적으로 작업을 진행할 수 있어 데이터베이스의 부하를 줄이고 성능을 개선할 수 있다.

     

    주의점

    벌크 연산 시 주의할 점이 있다. 벌크 연산은 영속성 컨텍스트를 거치지 않고 곧바로 쿼리를 실행한다는 점을 인지하고 사용해야 한다는 것이다. 다음 코드를 보자.

     

    Member

    @Entity
    @ToString(of = {"username", "age"})
    @NoArgsConstructor
    public class Member {
    
        @Id @GeneratedValue
        private Long id;
    
        private String username;
        private Integer age;
    
        public Member(String username, Integer age) {
            this.username = username;
            this.age = age;
        }
    }

     

    BulkOperationTest

    @SpringBootTest
    @Transactional
    public class BulkOperationTest {
    
        @PersistenceContext
        EntityManager em;
    
        @Autowired
        MemberRepository memberRepository;
    
        /**
         * 나이가 20살 이하인 사용자의 이름을 "hello"로 변경
         */
        @Test
        @Commit
        void bulkUpdate() {
            initData();
    
            em.createQuery("update Member m set m.username = 'hello' where m.age <= 20").executeUpdate();
    
            List<Member> result = memberRepository.findAll();
            for (Member member : result) {
                System.out.println("member = " + member);
            }
        }
    
        private void initData() {
            Member member1 = new Member("member1", 10);
            Member member2 = new Member("member2", 20);
            Member member3 = new Member("member3", 30);
            Member member4 = new Member("member4", 40);
            Member member5 = new Member("member5", 50);
    
            memberRepository.save(member1);
            memberRepository.save(member2);
            memberRepository.save(member3);
            memberRepository.save(member4);
            memberRepository.save(member5);
        }
    }

     

    위 코드의 println 결과를 보면 예상했던 것과 다르게 변경되지 않은 상태로 출력되는 것을 확인할 수 있다.

     

    출력 결과 (실제 DB와 불일치)

    member = Member(username=member1, age=10)
    member = Member(username=member2, age=20)
    member = Member(username=member3, age=30)
    member = Member(username=member4, age=40)
    member = Member(username=member5, age=50)

     

    실제 DB


    이러한 문제가 발생하는 이유는 벌크 연산이 영속성 컨텍스트를 거치지 않는 것과 관련이 있다. 수정 쿼리는 영속성 컨텍스트를 거치지 않고 곧바로 데이터베이스에 수정 쿼리가 실행된다. 따라서 영속성 컨텍스트는 Member의 데이터가 바뀐 것을 인지하지 못한다. findAll()을 통해 조회한 따끈따끈한 데이터를 사용하면 되지 않나? 라고 생각할 수 있는데 JPA는 그런 식으로 동작하지 않는다. 이미 영속성 컨텍스트의 1차 캐시에 동일한 PK를 가진 엔티티가 존재할 경우, 방금 가져온 데이터는 사용되지 않고 버려지기 때문이다. 따라서 아래와 같이 벌크 연산을 진행한 이후 영속성 컨텍스트를 초기화 해주는 것이 좋다.

    em.createQuery("update Member m set m.username = 'hello' where m.age <= 20").executeUpdate();
    
    em.flush();
    em.clear();
    
    List<Member> result = memberRepository.findAll();
    for (Member member : result) {
        System.out.println("member = " + member);
    }

     

    출력 결과 (실제 DB와 일치)

    member = Member(username=hello, age=10)
    member = Member(username=hello, age=20)
    member = Member(username=member3, age=30)
    member = Member(username=member4, age=40)
    member = Member(username=member5, age=50)