본문 바로가기

스프링 부트와 JPA

벌크(Vulk) 연산을 까먹지 말자!!(Feat. [수정], @Modifying)

벌크(Vulk) 연산을 평소에 잘 쓰지 않다보니, 자주 까먹는다.

고로, 게시물에 정리를 해서 안 까먹도록 하겠다. 

만약, 사원 Table에 100명의 사원이 있다고 하자. 

그리고 " [모든] 사원의 나이를 +1만큼 증가시켜라"라는 명령을 수행해야 한다고 하자. 

Brute Froce 씩으로 하면, for문을 돌려서 한 사원씩 조회를 하여, update 쿼리를 날리면 된다. 

그러나, 사원이 만약 100명이라면, 최소 100 번의 SQL문을 날리게 된다. 

이렇게 되면, DB 성능이 매우 나빠지게 된다. 

JPA에서는 이러한 명령을 1개의 SQL문으로 모든 100명의 사원들의 나이를 +1만큼 업데이트시키는 기능을 지원하는데 

그것이 바로 벌크(Vulk) 연산이다.

- JPA 이용하여 벌크 연산 -

//일단, 먼저 순수 JPA로 벌크 연산하는 메서드를 작성해보고, 나중에 Spring Data JPA로 더 편하게 작성을 해보자.
public int bulkAgePlus(int age){

    return entityManager.createQuery("UPDATE Member m set m.age = m.age + 1" +

                    " WHERE m.age >= :age") // selection condition
            .setParameter("age",age)
            .executeUpdate(); // 업데이트된 튜플의 개수를 반환!
}

 

@Test@Transactional
public void bulkUpdate(){

    repository.save(new Member("member1",10));
    repository.save(new Member("member2",19));
    repository.save(new Member("member3",20));
    repository.save(new Member("member4",21));
    repository.save(new Member("member5",40));

    //JPA의 dirty Checking으로 업데이트가 실행이 된다.
    int updated_row = repository.bulkAgePlus(20);// 20살 이상인 사람의 나이를 +1만큼 증가!

    assertThat(updated_row).isEqualTo(3);

}

 

- Spring Data JPA를 이용한 벌크 연산 -

public interface MemberReposiotry extends JpaRepository<Member,Long> {

	//이제는 순수 JPA가 아닌, Spring Data JPA 방식으로 벌크 연산을 구현!
     @Modifying // Spring Data JPA는 이게 없으면, getResultList()나 getResultSingle()을 호출해 버림
     // @Modifying이 있어야, executeUpdate()가 호출됨.
     @Query("UPDATE Member m set m.age = m.age + 1 WHERE m.age >= :age")
     public int bulkAgePlus(@Param("age") int age);

}

 

@Test@Transactional
public void bulkUpdate(){

    repository.save(new Member("member1",10));
    repository.save(new Member("member2",19));
    repository.save(new Member("member3",20));
    repository.save(new Member("member4",21));
    repository.save(new Member("member5",40));

    // bulkAgePlus()의 executeUpdate()에 의해 Context가 flush()된다. 
    int updated_row = repository.bulkAgePlus(20);// 20살 이상인 사람의 나이를 +1만큼 증가!

    assertThat(updated_row).isEqualTo(3);

}

 

그러나 순수 JPA이든, Spring Data JPA이든 벌크 연산 시 주의해야 할 점이 있다. 

둘 다 결국에는 Context를 사용을 하는데, executeUpdate()를 만나는 순간, 그 업데이트 쿼리문은 DB에 날라 간다.

(Context의 다른 sql문은 그대로 남아 있음)

그렇게 되면, 데이터의 불일치 문제 등 여러 가지 문제가 발생할 수가 있다. (아래 코드 참조)

@Test@Transactional
    public void bulkUpdate(){

        repository.save(new Member("member1",10));
        repository.save(new Member("member2",19));
        repository.save(new Member("member3",20));
        repository.save(new Member("member4",21));
        repository.save(new Member("member5",40));


        int updated_row = repository.bulkAgePlus(20);// 20살 이상인 사람의 나이를 +1만큼 증가!
        entityManager.clear(); // 벌크 연산 시, 꼭 Context를 초기화시켜 줘야 데이터 불일치 같은 문제가 안 생긴다. 

        
        // Context가 clear()된 것은 아니기에, member5를 조회를 하면 +1이 반연되지 않은 40살이 조회된다.
        // 그러나 DB에는 이미 41살로 업데이트가 완료가 됨 -> [데이터 불일치] 문제
        // JPA의 AUTO COMMIT 전략 중 하나로, ExecuteUpdate()처럼 즉시, 쿼리문이 날라가는 경우
        // -> JPA가 먼저 Context에 있는 모든 sql문을 flush()를 해주고, 그 다음 update문을 날리게 된다. 
        List<Member> member5 = repository.findByUsername("member5");
        Member member = member5.get(0);
        System.out.println("member = " + member);  // 40살이 출력됨.


        assertThat(updated_row).isEqualTo(3);

 

해결책 1: clear() 을 사용하여 Context를 초기화해준다. 

@Modifying(clearAutomatically = true) // TX가 끝나기 전에, 쿼리가 날라갈 일이 생기면
					                  // 자동으로 entityManager.Clear()를 해줌. 
@Query("UPDATE Member m set m.age = m.age + 1 WHERE m.age >= :age")
public int bulkAgePlus(@Param("age") int age);

 

해결책 2 : @Modifying(clearAutomatically = true) 이용!!

@Modifying(clearAutomatically = true) // TX가 끝나기 전에, 쿼리가 날라갈 일이 생기면
// 자동으로 entityManager.Clear()를 해줌. 
@Query("UPDATE Member m set m.age = m.age + 1 WHERE m.age >= :age")
public int bulkAgePlus(@Param("age") int age);