벌크 연산이란 여려 데이터를 한 번에 수정하거나 삭제하는 방법이다.
실제 벌크 연산을 하지 않을 경우, 어떤 성능적 이슈가 생기는지 예시를 통해서 알아보자.
@Entity
@AllArgsConstructor
@NoArgsConstructor
class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
public void getOlder() {
this.age += 1;
}
}
Member member1 = new Member("john", 15);
Member member2 = new Member("tom", 19);
Member member3 = new Member("risa", 22);
Member member4 = new Member("bob", 23);
Member member5 = new Member("sera", 26);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
em.persist(member5);
em.flush();
em.clear();
위의 코드를 실행하면, 아래와 같이 5개의 데이터가 저장된다.
MEMBER_ID | NAME | AGE |
1 | john | 15 |
2 | tom | 19 |
3 | risa | 22 |
4 | bob | 23 |
5 | sera | 26 |
만약 새해가 밝아, 모든 멤버들의 나이를 한 살 더 해주기 위해 다음과 같이 코드를 작성해 볼 수 있다.
// 트랜잭션 시작
Member findMember1 = em.find(Member.class, 1L);
Member findMember2 = em.find(Member.class, 2L);
Member findMember3 = em.find(Member.class, 3L);
Member findMember4 = em.find(Member.class, 4L);
Member findMember5 = em.find(Member.class, 5L);
findMember1.getOlder();
findMember2.getOlder();
findMember3.getOlder();
findMember4.getOlder();
findMember5.getOlder();
// 트랜잭션 커밋
위의 코드를 이해하기 위해서는 JPA의 Dirty Checking에 대해서 알아야 한다.
Dirty Checking
JPA에는 실제 update sql와 관련된 메서드가 존재하지 않는다.
Spring JPA에서는 데이터를 수정하려면, 영속성 컨텍스트의 변경 감지(Dirty Checking)나 병합을 사용해야 한다.
Dirty Checking의 동작 순서는 다음과 같다.
- 트랜잭션 시작, 영속성 컨텍스트 초기화
- 엔티티 조회 후, 조회된 상태의 엔티티에 대한 스냅샷 생성
- 비즈니스 로직
- 트랜잭션 커밋 후, 스냅샷과 현재 엔티티 상태를 비교하여 update 쿼리 전달
따라서 Dirty Checking을 검사하는 대상은 영속성 컨텍스트가 관리하는 엔티티를 대상으로 한다. 준영속, 비영속 엔티티는 값을 변경할지라도 데이터베이스에 반영하지 않는다.
다시 돌아와서,
위의 코드를 실행시키면, 과연 몇 개의 update 쿼리가 데이터베이스에 전달될까?
정답은 총 5개의 update 쿼리가 데이터베이스에 전달된다. 만약 멤버 데이터가 5개가 아닌 더 많은 데이터를 변경해야 한다며, 멤버 데이터 개수만큼 update 쿼리가 전달될 것이다. 이는 매우 비효율적인 방법이며, 성능상 이슈가 발생한다.
이를 해결하는 방법이 벌크 연산이다. 벌크 연산을 통해 여러 데이터를 하나의 쿼리로 수정이 가능하다.
em.createQuery("UPDATE Member m SET m.age = m.age + 1")
.executeUpdate();
다음과 같이 JPQL를 통해 한방에 모든 멤버 데이터의 나이를 1 증가시킬 수 있다.
Spring Data JPA에서는 벌크 연산 쿼리를 사용하기 위해서 @Modifying 어노테이션을 사용해야 한다.
@Modifying
@Query("UPDATE Member m SET m.age = m.age + 1")
int getOlder;
만약 벌크 연산을 실행한 이후 영속성 컨텍스트를 초기화하고 싶으면, clearAutomatically 옵션을 true로 설정하면 된다. 이 옵션은 기본값은 false이다.
@Modifying(clearAutomatically = true)
벌크 연산을 사용 시 주의할 점이 있다.
벌크 연산을 사용하기 위해 JPQL을 사용하는데, JPQL은 영속성 컨텍스트를 지나치지 않고 바로 데이터베이스에 쿼리문을 보낸다.
따라서 영속성 컨텍스트와 2차 캐시를 무시하고 데이터베이스에 직접 실행시키기 때문에, 데이터베이스간에 데이터 차이가 발생할 수 있다.
- em.refresh
- 벌크연산을 먼저 실행
- 벌크연산 수행 후 영속성 컨텍스트 초기화
를 통해 영속성 컨텍스트와 데이터베이스 간의 데이터 차이를 없애주어야 한다.
사실 JPQL로 인해 발생하는 문제라고 생각해도 무방할 것 같다. 따라서 JPQL의 동작원리에 대해서도 알아야 한다.. JPQL에 대해서는 다음 글에 포스팅할 예정이다.
'개발 > Spring' 카테고리의 다른 글
[Spring] Spring Boot 3 사용해보자! (0) | 2023.06.10 |
---|---|
JWT (2) 스프링에서 JWT 사용하기 (0) | 2023.05.31 |
[Spring] RestTemplate 싱글톤 등록 및 Connection Pool 설정 (0) | 2023.05.14 |
[Spring] 간편 결제 기능 팩토리 클래스 적용하기 (0) | 2023.05.02 |
[Spring] 동일한 클래스 내에서 내부 메서드 호출시 @Transactional 적용 안되는 이슈 (0) | 2023.04.02 |