반응형
05-15 00:00
Today
Total
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
관리 메뉴

개발하는 고라니

[JPA] 벌크 연산 (Bulk Operation) 본문

Framework/JPA (Hibernate)

[JPA] 벌크 연산 (Bulk Operation)

조용한고라니 2021. 8. 5. 17:55
반응형

진행하기 앞서...

RDB에 Member라는 테이블이 있고, PK는 id이고 salary라는 컬럼이 존재하며 이는 연봉을 나타낸다. 

 

연봉 3000만원 미만의 Member의 salary를 일정 크기만큼 인상한다면 어떻게 SQL을 짜겠는가?

나라면,

UPDATE MEMBER SET SALARY = SALARY * 1.3 WHERE SALARY < 30000000

처럼 짤 것이다.

 

이제 JPA의 관점에서 생각해보자.

우리가 JPA에서 특정 엔티티의 값을 변경하려면 어떻게 해야했는가?

 

1. em.find() OR select 쿼리를 날려 영속성 컨텍스트에 엔티티 저장 후 반환

2. 반환 받은 엔티티의 값을 변경한다. -> 영속성 컨텍스트에 반영된다.

3. Commit 시점에 변경 감지(Dirty Checking)이 일어나며 Update 쿼리를 날려 DB에 반영한다.

 

그렇다. JPA에서 DB에 UPDATE 쿼리를 날리기 위해선 변경 감지를 이용했었다. 근데 만약 Member가 80만명 존재하고, 연봉이 3000만원 미만인 Member가 60만명이라면?

 

60만번의 더티 체킹이 일어날 것이며, 이는 60만번의 UPDATE 쿼리가 날아갈 것이다. (상상만해도 끔찍하다)

 

실제로 테스트를 해보도록 하자.

변경 감지 테스트

//1. 100명의 사람을 저장한다. (나이는 0부터 100까지)
IntStream.rangeClosed(0, 100).forEach(i -> {
                em.persist(Member.builder()
                        .username("test")
                        .age(i)
                        .build()
                );
            });
//2. 영속성 컨텍스트를 초기화하고, 나이가 90살 미만인 사람을 조회한다.
em.clear();

List<Member> members = em.createQuery("select m From Member m where m.age < 90", Member.class)
                    .getResultList();
                    
System.out.println("90살 미만인 사람 수 = " + members.size() + "명"); //90명

//3. 그 사람들의 나이를 100살로 변경
members.forEach(i -> i.changeAge(100));

tx.commit();

 

SQL 로그를 확인해보자.

(1)의 insert는 무시하고 넘어간다.

 

(2)

    /* select m From Member m where m.age < 90 */

select
  member0_.member_id as member_i1_0_,
  member0_.age as age2_0_,
  member0_.team_id as team_id4_0_,
  member0_.username as username3_0_ 
from
  Member member0_ 
where
  member0_.age < 90

 

(3)

    /* update
        domain.Member */ update
            Member 
        set
            age=?,
            team_id=?,
            username=? 
        where
            member_id=?
17:31:23.150 [main] DEBUG org.hibernate.SQL - 
    /* update
        domain.Member */ update
            Member 
        set
            age=?,
            team_id=?,
            username=? 
        where
            member_id=?
....
//이 쿼리가 90번 나간다.

벌크 연산

이러한 문제점을 확인했으니 해결책을 찾아야 한다.

그 해결책은 벌크 연산이다. 하지만 이도 잘 써야하며, 고려해야할 점이 존재한다.

 

벌크 연산은 쿼리 한번으로 여러 테이블 row를 변경한다.

 

int resultCount = em.createQuery(query, class).executeUpdate();

//resultCount는 변경된 row 수를 반환받는다.
//UPDATE / DELETE 문을 지원한다.
//hibernate는 INSERT 문도 지원한다.

 

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 날린다.

 

따라서 사용 가이드는 다음과 같다.

 

1) 벌크 연산을 먼저 수행

2) 벌크 연산을 수행 후 영속성 컨텍스트를 비운다.

 

※ 아까의 테스트 예제에서 (2)와 (3) 부분을 지우고 다음으로 수정해본다.

            int resultCount = em.createQuery("update Member m set m.age = 100 where m.age < :age")
                    .setParameter("age", 90)
                    .executeUpdate();

            System.out.println("resultCount = " + resultCount);

 

SQL 로그)

    /* update
        Member m 
    set
        m.age = 100 
    where
        m.age < :age */ update
            Member 
        set
            age=100 
        where
            age<?

단 한번의 쿼리가 나간다!

주의사항

※ 벌크 연산은 영속성 컨텍스트를 무시하고 (영향을 주지않고) DB에 직접 쿼리를 날린다. 이로 인해 주의해야할 점을 살펴보자.

 

1) Member를 저장할 때 age를 모두 20으로 설정하고 저장한다.

 

2) 영속성 컨텍스트를 비운다.

em.clear();

 

3) 모든 멤버를 조회한다.

List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();

 

4) 벌크 연산으로 모든 멤버의 나이를 50살로 변경한다.

int resultCount = em.createQuery("update Member m set m.age = 50").executeUpdate();

 

5) DB에 저장된 멤버들의 나이를 확인한다.

 

6) 멤버들의 나이를 출력해본다.

members.forEach(m -> { System.out.println("m.getAge() = " + m.getAge()); });

m.getAge() = 20
m.getAge() = 20
m.getAge() = 20
m.getAge() = 20
m.getAge() = 20
m.getAge() = 20
m.getAge() = 20
m.getAge() = 20
m.getAge() = 20
m.getAge() = 20
...

 

결과) 

DB에 저장된 값과, 영속성 컨텍스트에 담긴 값이 달라 데이터 정합성에 어긋난다.

그러므로

- 벌크 연산을 먼저 수행

 

- 벌크 연산 수행후 영속성 컨텍스트 초기화

 

둘 중 자신의 상황에 맞는 가이드를 사용해서 이런 이슈를 피하도록 하자.

 

#Reference

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 본 강의는 자바 백엔

www.inflearn.com

인프런 - 김영한님의 JPA 강의 기본편 - 벌크 연산

반응형

'Framework > JPA (Hibernate)' 카테고리의 다른 글

[JPA] Spring Data JPA  (0) 2021.10.09
[JPA] OSIV (Open Session In View)  (2) 2021.09.19
[JPA] 페치 조인 (FETCH JOIN)  (0) 2021.08.05
[JPA] Cascade / OrphanRemoval  (0) 2021.07.25
[JPA] Proxy  (0) 2021.07.23
Comments