안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 SpringBoot의 Data JPA 벌크연산에 대한 글을 작성하고자 합니다.

 

1. 벌크 연산

벌크 연산은 여러 건의 데이터를 한 번에 수정하거나 삭제하는 방법으로 대용량 데이터를 한 번에 처리할 때 유용합니다.

 

현재 주어진 Member table에는 6개의 행이 존재합니다. 만약 25살 이상의 나이에 모두 1살을 더하라는 요청이 오면 , 다음과 같은 쿼리를 생성할 수 있습니다.

update member m
set m.age = m.age + 1
where m.age >= 25;

혹은 Member table에서 member_id가 5 이상인 행을 제거하라는 요청이 오면 쿼리는 다음과 같습니다.

delete from member m
where m.member_id >= 5;

 

2. 스프링 DATA JPA의 벌크연산

이처럼 한 번에 다량의 데이터를 수정하는 것을 벌크연산이라고 하는데, 스프링 데이터 JPA에서는 벌크연산을 지원합니다.

@Modifying 어노테이션과 함께 벌크 연산 쿼리를 작성하면 해결할 수 있습니다.

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = PROTECTED)
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String username;

    private int age;
}
@Repository
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);
    
}

 

3. 테스트 코드로 확인하기

@Test
public void bulkUpdate() {
    //given
    memberJpaRepository.save(new Member("member1", 9));
    memberJpaRepository.save(new Member("member2", 9));
    memberJpaRepository.save(new Member("member3", 9));
    memberJpaRepository.save(new Member("member4", 15));
    memberJpaRepository.save(new Member("member5", 40));

    //when
    int resultCount = memberJpaRepository.bulkAgePlus(15);
    
    //then
    assertThat(resultCount).isEqualTo(2);
}

벌크 연산을 수행하면 15살 이상의 나이가 수정이 되는 것을 확인할 수 있습니다.

 

4. 주의점

이 글의 목적이기도 한 벌크 연산의 주의점입니다. 벌크 연산은 영속성 컨텍스트를 참조하지 않고 DB에 직접 접근하기 때문에 1차 캐시에 남아있는 캐시와 값이 다를 수 있습니다. 따라서, 만약 영속성 컨텍스트를 초기화하지 않는다면, 변경 이전의 값을 참조할 수 있다는 위험성이 존재합니다.

 

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {

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

@Modifying(clearAutomatically = true)는 해당 쿼리가 실행 된 후, 영속성 컨텍스트를 초기화하는 역할을 수행합니다.  만약 (clearAutomatically = true)가 없다면 벌크 연산 이후에 영속성 컨텍스트에 남아있는 이전의 값이 활용되는 문제점 발생합니다.

@Test
public void bulkUpdate2() {
    //given
    memberRepository.save(new Member("member1", 9));
    memberRepository.save(new Member("member2", 9));
    memberRepository.save(new Member("member3", 9));
    memberRepository.save(new Member("member4", 15));
    memberRepository.save(new Member("member5", 40));

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

    Member member5 = memberRepository.findMemberByUsername("member5");

    //then
    assertThat(resultCount).isEqualTo(2);
    assertThat(member5.getAge()).isEqualTo(40);
}

따라서 이러한 문제점을 예방하기 위해  벌크연산 이후에 값을 flush 하고, 초기화 해주어야 합니다.

 

@Test
public void bulkUpdate2() {
    //given
    memberRepository.save(new Member("member1", 9));
    memberRepository.save(new Member("member2", 9));
    memberRepository.save(new Member("member3", 9));
    memberRepository.save(new Member("member4", 15));
    memberRepository.save(new Member("member5", 40));

    //when
    int resultCount = memberRepository.bulkAgePlus(15);
    em.flush();
    em.clear();

    Member member5 = memberRepository.findMemberByUsername("member5");

    //then
    assertThat(resultCount).isEqualTo(2);
    assertThat(member5.getAge()).isEqualTo(41);
}

 

그런데, 이 과정에서 들었던 궁금증은 flush()하게 되면 다시 영속성 컨텍스트에 있는 40이 데이터베이스에 반영되어 41 -> 40이 되지 않을까?라는 생각을 하게 되었습니다.

 

이에 대한 해답은 영한님께서 제공해주셨습니다.

인프런에 올라온 질문에 대한 답글을 참조하면, 다음과 같습니다.

엔티티에 직접적인 변경내용이 있어야 flush() 시점에 변경 감지의 대상이 되어서, 변경된 내용을 반영합니다. 그런데 벌크 연산은 엔티티에 영향을 주지 않아서 이런 결과가 나옵니다.

즉, 영속성 컨텍스트에 영향을 받는 엔티티가 직접적으로 수정이 되지 않았으므로 더티 체킹에 해당하지 않습니다. 따라서, 업데이트 쿼리가 실행되더라도 영속성 컨텍스트의 flush()가 다시 16 -> 15로 수정하지 않는 것입니다.

 

5. 정리

벌크 연산을 수행할 때는, 다른 데이터에 영향을 주지 않는지 파악하여 영속성 컨텍스트를 clear 하는 것을 기억해야 합니다.!

 

이상입니다.

부족한 부분에 대해서 댓글 부탁드립니다.

감사합니다!!!

 

참고 자료: 영한님 실전! 스프링 데이터 JPA https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84

+ Recent posts