- Today
- Total
개발하는 고라니
[JPA] 벌크 연산 (Bulk Operation) 본문
진행하기 앞서...
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
인프런 - 김영한님의 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 |