본문 바로가기
Spring/JPA

JPA 벌크 연산 처리 방법 및 주의점

by YoonJong 2023. 2. 25.
728x90

JPA 에서 벌크 연산을 하는 방법에 대해 알아보겠습니다.

먼저, 벌크 연산은 말 그대로 어떤 조건이나 상황에 따라 한번에 연산(수정) 하는 것을 의미합니다.

 

가장 중요한 개념은, 벌크 연산 시 SQL 을 실행하는 것이기 때문에 1차 캐시를 거치지 않고 바로 DB 로 접근해서 값을 수정합니다. 

따라서, 1차 캐시를 초기화 하지 않으면, 수정한 데이터의 DB 값과 1차 캐시에 있는 값의 정합성이 다르게 됩니다.


예시는 스프링 데이터 JPA 강의 및 QueryDsl 내용을 참고했습니다.

 

2가지 방법으로 나뉩니다.

스프링 데이터 JPA 를 사용하지 않는 방법스프링 데이터 JPA 를 사용한 방법으로 나뉩니다.

 

먼저 첫번째 스프링 데이터 JPA 를 사용하지 않는 방법입니다.

예시는 정해진 age 보다 크거나 같을 경우 현재 나이에서 1살을 더해주는 것입니다.

jpql 을 사용해서 쿼리를 작성해 줄 수 있습니다.

public int bulkAgePlus(int age) {
    return em.createQuery("update Member m set m.age = m.age +1" +
                    " where m.age >= :age")
            .setParameter("age", age)
            .executeUpdate();
}

아래는 테스트 코드입니다.

총 5명의 회원이 있는데 조건인 20살보다 많은 사람은 총 3명입니다.

assertThat 으로 확인해보면 테스트가 성공합니다.

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

    int resultCount = memberJpaRepository.bulkAgePlus(20);

    assertThat(resultCount).isEqualTo(3);
}

 

두 번째, 스프링 데이터 JPA 를 사용하는 방법을 확인해보겠습니다.

@Query 와 jpql 을 사용해서 작성해주었습니다.

벌크연산을 진행할때는 @Modifying을 붙여주어야 합니다.

 

@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

테스트 코드를 진행해보면 정상적으로 성공을 합니다.

@Test
void bulkAgePlus() {
    //given
    memberRepository.save(new Member("member1", 10));
    memberRepository.save(new Member("member3", 19));
    memberRepository.save(new Member("member3", 20));
    memberRepository.save(new Member("member4", 21));
    memberRepository.save(new Member("member5", 40));

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

    List<Member> findMember = memberRepository.findByUsername("member5");
    Member member = findMember.get(0);
    System.out.println("member = " + member);

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

데이터베이스를 확인해보겠습니다.

조건에 맞는 나이가 1씩 증가한 것을 확인할 수 있습니다.

 

이번 테스트 코드에서는 member5 를 find 해서 어떤 값이 나올지 출력해보았는데 어떤 값이 출력될까요?

여기서 DB 와 다른 데이터가 출력됩니다.

출력된 결과를 보면 나이가 40 입니다. DB 와 다른 데이터가 출력되었습니다.

 

이유가 무엇일까요??

바로 영속성 컨텍스트가 초기화 되지 않았기 때문입니다.

 

bulkAgePlus 가 실행될 때 벌크연산은 영속성 컨텍스트를 무시하고 DB 값이 바로 수정되어버립니다.

현재 영속성컨텍스트의 member5 의 나이는 40살인데, DB 값은 41살인 상황이 되어버려 서로의 값 상태가 달라진 것 입니다.

 

이를 해결하기 위해서는 2가지 방법이 존재합니다.

엔티티 매니저를 만들어 flush() 를 해서 DB에 저장을 하고 clear()를 진행해 초기화를 진행합니다.

@PersistenceContext
EntityManager em;
...

int resultCount = memberRepository.bulkAgePlus(20);
em.flush(); // 저장
em.clear(); // 초기화

정상적으로 DB 와 같은 값이 조회되는 것을 볼 수 있습니다.

 

두번째로, 어노테이션을 이용한 방법입니다.

@Modifying(clearAutomatically=true) 설정을 해주면 동일한 값을 조회할 수 있습니다.

@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

728x90

댓글