JAVA/JPA 학습기록

[JPA] 벌크 Update, Delete 연산과 영속성 컨텍스트

kth990303 2022. 9. 15. 17:34
반응형

벌크 수정연산과 벌크 삭제연산을 각각 수행한 후에 별도로 영속성 컨텍스트 초기화 작업을 하지 않았다고 가정하자.

이후에 전체조회 작업을 시행하면 update는 연산이 적용되지 않은 영속성 컨텍스트 데이터, delete는 연산이 적용된 DB 데이터 결과가 조회되는 것을 확인할 수 있다.

 

왜 그런 것일까?

 

이를 이해하기 위해선 영속성 컨텍스트를 제대로 이해하고 있어야 한다.

아래 예시와 함께 살펴보도록 하자.


Update 벌크연산 상황

영속성 컨텍스트와 DB에 아래 데이터가 있다고 하자.

위 상황에서 아래와 같이 28살 미만은 이름을 전부 비회원으로 바꾸라는 bulk update 연산을 수행해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void bulkUpdate() {
    factory
            .update(member)
            .set(member.username, "비회원")
            .where(member.age.lt(28)) // 28살 미만은 이름을 전부 비회원으로 바꿔라
            .execute();
 
    final List<Member> result = factory
            .selectFrom(member)
            .fetch();
 
    assertThat(result.get(0).getUsername()).isEqualTo("비회원");
}
cs

벌크 연산은 영속성 컨텍스트가 아닌 실제 DB에 바로 쿼리를 날린다.

그렇기 때문에 작업을 수행한 후, em.clear()로 영속성 컨텍스트를 초기화해주어야 한다.

그렇지 않으면 영속성 컨텍스트와 DB는 서로 다른 데이터가 저장돼있을 것이다.

 

위 테스트 코드에서는 update 벌크연산 이후에 별도의 영속성 컨텍스트 초기화 작업을 해주지 않았다.

따라서 아래와 같이 데이터가 저장돼있을 것이다.

 

벌크 update 연산 이후의 데이터 상태. 실제 DB에만 update가 반영된다.

이 상태에서 queryDSL의 selectFrom() 함수를 수행하면 영속성 컨텍스트의 1차 캐시에서 값을 가져오기 때문에 member1은 변경된 이름인 비회원이 아닌 member1이라는 값으로 조회된다.

테스트 실패. 10살인 member1의 이름이 `비회원`으로 바뀌지 않고 member1으로 조회된다.

따라서 위 테스트는 사진과 같이 실패하게 된다.


Delete 벌크연산 상황

이번에는 벌크 delete 연산을 살펴보자.

아래와 같이 18살 초과의 데이터는 모두 지우라는 벌크 연산을 날린다고 해보자. 

1
2
3
4
factory
        .delete(member)
        .where(member.age.gt(18)) // 18살 초과는 모두 지워라
        .execute();
cs

여러 데이터를 한번의 쿼리만으로 삭제시키는 bulk delete 쿼리이다.

벌크 연산은 영속성 컨텍스트가 아닌 실제 DB에 바로 쿼리를 날린다.

따라서 해당 작업이 수행되면 아래와 같은 상태가 될 것이다.

 

아까와 마찬가지로 queryDSL의 selectFrom()을 수행하여 결과를 아래와 같이 테스트해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void bulkDelete() {
    factory
            .delete(member)
            .where(member.age.gt(18)) // 18살 초과는 모두 지워라
            .execute();
 
    final List<Member> result = factory
            .selectFrom(member)
            .fetch();
 
    assertThat(result).hasSize(4);
}
cs

영속성 컨텍스트의 1차 캐시에는 삭제 쿼리가 반영되지 않아 모든 데이터가 존재하므로 findAll한 반환값 List<Member>의 사이즈는 4가 나올 것이라 예상된다.

 

하지만 테스트를 실행해보면?

테스트 실패. 4건의 데이터가 아닌 1건의 데이터만 나온다.

우리는 4개의 데이터가 나올 줄 알았는데 실제로는 1건의 데이터가 나왔다!

분명 영속성 컨텍스트에는 4개의 데이터가 다 남아있음에도 불구하고 말이다.


Delete 벌크연산 상황 해답 및 영속성 컨텍스트 원리

이러한 가설을 세울 수도 있겠다.

 

가설) 벌크 연산은 delete일 때는 영속성 컨텍스트에도 반영을 하는

말도 안되는 일이 일어난 것이다.

 

과연 이 가설은 맞을까?

 

그렇지는 않다.

 

영속성 컨텍스트는 그대로 4개의 데이터가 존재하는 것이 맞다.

이는 아래 테스트로 확인해볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public voidbulkDelete() {
        factory
                    .delete(member)
                    .where(member.age.gt(18)) // 18살 초과는 모두 지워라
                    .execute();
        
        // em.find -> JPQL이 아니므로 영속성 컨텍스트에서 member3을 찾는다
        final Member findMember3 = em.find(Member.class, member3.getId());
 
    assertThat(findMember3.getUsername()).isEqualTo("member3");
}
cs

EntityManager의 find함수를 수행하면 영속성 컨텍스트에서 member3을 찾는다.

member3은 30살이고, 따라서 벌크 delete 연산으로 지워져야되는 대상이다.

 

해당 연산으로 영속성 컨텍스트에 member3이 없다면 해당 테스트는 실패하게 될 것이다.

 

테스트 성공

테스트가 통과하는 것을 확인할 수 있다.

 

위와 같이 영속성 컨텍스트에서 member3을 찾아오는데 성공했기 때문이다.

따라서 영속성 컨텍스트엔 삭제 쿼리가 반영되지 않은 상태로 4개의 데이터가 존재하는 것이 맞다.

 

근데 왜 queryDSL의 selectFrom() 작업 수행 결과로는 1개의 데이터만 찾아오는 것일까?

 

일단 queryDSL의 selectFrom() 작업은 스프링 데이터 JPA의 findAll과 마찬가지로 JPQL을 날린다.

JPQL은 영속성 컨텍스트의 1차 캐시를 탐색하여 동일성을 보장하는 JPA의 findById 또는 EntityManager의 find() 함수와는 다르게 작동한다.

 

JPQL을 호출하면 아래와 같이 동작한다.

 

  1. 실제 DB를 우선 조회한다.
  2. DB에 조회한 값을 영속성 컨텍스트에 저장한다.
  3. 저장할 때 고유식별자(ex. pk)로 이미 영속성 컨텍스트에 해당 엔티티가 존재할 경우 JPA의 동일성 보장을 위해 DB의 데이터를 버리고 영속성 컨텍스트의 데이터를 반환하여 사용한다.

따라서 queryDSL의 selectFrom()을 수행하면 JPQL을 호출하여 실제 DB를 먼저 조회한다. 실제 DB에는 member1에 해당하는 엔티티가 존재하고, 영속성 컨텍스트에 해당 값을 저장한다.

 

이 때, member1에 해당하는 엔티티가 영속성 컨텍스트에 존재하므로 실제 DB의 데이터 대신 영속성 컨텍스트에 존재하는 데이터를 사용하게 된다. member2, member3, member4는 실제 DB에 존재하지 않기 때문에 영속성 컨텍스트를 확인할 때 조회 대상에 포함되지 않게 된다. 따라서 selectFrom을 한 최종 결과는 member1만 반환하게 된다.

 

따라서 해당 테스트의 결과는 4가 아닌 1이다.


Update 벌크연산 상황 해답

update는 어떻게 동작한 것일까?

 

update의 경우는 해당 엔티티가 삭제된 것이 아니기 때문에 실제 DB와 영속성 컨텍스트에 모두 데이터가 남아있는 상태이다.

 

따라서 JPQL을 호출하면 아래와 같은 원리로 작동된다.

 

  1. 실제 DB를 우선 조회한다.
  2. 실제 DB에 존재하는 4개의 데이터를 영속성 컨텍스트에서 찾는다. bulk update 연산으로 고유식별자는 변하지 않으므로 영속성 컨텍스트에서 4개의 데이터를 모두 발견한다.
  3. 따라서 실제 DB의 데이터 대신 update되지 않은 영속성 컨텍스트의 데이터 4개를 반환한다.

그렇기 때문에 위에서 본 상황과 같이 벌크연산이 적용되지 않은 영속성 컨텍스트 데이터를 반환하게 되어 아래와 같은 테스트 결과를 보게 되는 것이다.

테스트 실패. 10살인 member1의 이름이 `비회원`으로 바뀌지 않고 member1으로 조회된다.


결론

벌크 연산은 영속성 컨텍스트가 아닌 실제 DB에 바로 쿼리를 날린다.

영속성 컨텍스트의 원리를 제대로 이해하지 못한다면 결과에 대해 이해하지 못할 수 있다.

영속성 컨텍스트의 원리를 확실하게 공부하자.

 

또한, 벌크 연산을 사용한다면 영속성 컨텍스트를 초기화해주는 em.clear(), 그리고 다른 추가적인 작업이 있다면 em.flush()를 해주는 것을 잊지 말도록 하자.

 

참고

 

도움을 준 사람

반응형