[JPA] commit, flush, Entity Manager의 clear()와 close()에서 궁금한 부분들 탐구 + 데이터 삭제 및 수정 시 1차 캐시에서 발생하는 현상 + 준영속과 비영속의 차이점

Benjamin·2023년 6월 15일
6

JPA

목록 보기
3/4

🙋🏻‍♀️JPA에 관해 공부를 하다가 문득 궁금한게 생겼다.

우선 flush와 commit에 대해 어떤 내용을 공부했는지 간단히 살펴보자 !

  • 영속 컨텍스트는 엔티티를 식별자 값으로 구분한다.
    따라서 영속 상태는 식별자 값이 반드시 있어야 한다.(@Id로 테이블의 기본키와 매핑한 값)

트랜잭션을 커밋하는 순간 flush 발생

  • commit을 하는 순간, flush가 되면서 쿼리가 날라가고 실제 DB에 반영된다고 한다.

변경 감지에서의 flush

  1. JPA는 커밋하는 시점에 내부적으로 flush()가 호출된다
  2. 영속성 컨텍스트 flush() 호출되면, JPA는 1차캐시에 저장된 엔티티와 스냅샷을 비교한다
    (스냅샷 = 최초로 읽어온 그 시점의 상태를 스냅샷으로 떠둔다)
  3. 만약 스냅샷과 다른 부분이 있다면, JPA는 UPDATE 쿼리를 쓰기지연 SQL저장소에 저장한다
  4. flush가 일어나며 해당 쿼리를 DB에 반영한다.
  5. commit

위의 두 상황을 놓고봤을 때 flush에 관해 궁금한점이 있다. (빨간 박스 체크)

❓ dirty checking에서는 트랜잭션 커밋 시점에 엔티티 매니저 내부에서 flush()가 호출된다고 했는데, 이 때 발생하는 flush()와 쓰기 지연 시 호출되는 flush는 같은 것이라고 봐도 될까?
flush는 1차 캐시와 DB 동기화를 위해 쿼리를 날린다고 배웠는데, 그럼 Dirty checking에서는 쿼리를 2번 날리는건가?
헷갈린다.

우선 Flush가 무엇인지 정확히 짚고 넘어가보자.

Flush

  • 영속성 컨텍스트의 변경내용을 데이터 베이스에 반영
    -> SQL이 DB로 날라가며, 즉 영속 컨텍스트의 변경 사항과 DB를 맞추는 작업을 수행한다.

  • 데이터베이스에는 트랜잭션이라는 작업 단위가 존재하기때문에, 한 트랜잭션 내에서 아무리 flush가 발생해도 DB commit은 발생하지 않는다. DB commit은 해당 트랜잭션이 commit될 때 발생한다.

다시 돌아와서 궁금했던 사항에 대해 살펴보자.

트랜잭션 commit 시점에 내부적으로 호출되는 flush()와 쓰기 지연에서 호출되는 flush의 차이점?

✏️ 결론부터 말하자면, flush()는 어떤 작업들을 수행하는 것이고, flush는 그 수행 작업들 중 하나다.

트랜잭션 커밋시 내부적으로 flush() 호출

  1. 엔티티와 스냅샷 비교 후 변경된 것에 대한 SQL 생성
  2. 생성된 SQL을 쓰기 지연 SQL 저장소에 등록
  3. 쓰기 지연 SQL 저장소에 등록된 쿼리를 DB로 전송

1,2번 항목은 3번을 위해 필요한 과정이다.
변경 내용을 DB에 반영하는 것만 놓고 보면 3번을 flush라고 볼 수 있는것이고, 그 과정에서 필요한 것들이 1-2번이다.

Transaction Commit시 flush가 자동호출 되는 이유

  • flush() 는 우리가 DB Editor 에서 SQL 을 작성하는것이라고 할 수 있으며, 트랜잭션 커밋은 말 그대로 COMMIT 을 수행하는것이다. SQL을 쓰지 않고 COMMIT을 수행한다면 당연히 어떤 일도 일어나지 않는다.
    트랜잭션 커밋시 flush() 가 자동호출 되는 이유가 바로 이 때문이다.

🔥 flush는 영속 컨텍스트를 비우지않는다!

정확히 말하면, flush는 변경사항을 DB와 동기화하는 것을 의미하기때문에 flush가 발생해도

  • 1차 캐시가 유지된다. 🔥
  • 쓰기 지연 SQL 저장소의 쿼리문도 지워지지 않는다. 🔥

-> Flush 후에도 영속성 컨텍스트는 엔티티들을 관리하고, 1차 캐시(1st-level Cache)와 같은 장점을 가지고 있다. (이후에 다른 작업이 있을 때 해당 엔티티들을 재사용할 수 있다)

단, 트랜잭션을 커밋하고 영속성 컨텍스트를 완전히 초기화하려면 트랜잭션의 범위가 종료되어야 한다.
범위가 종료되면 영속성 컨텍스트는 완전히 비워지고, 다음에 필요한 경우 새로운 트랜잭션과 함께 새로운 영속성 컨텍스트가 생성된다.

그럼, commit을 할 때 영속 컨텍스트는 어떻게될까? 🧐

🖍 commit은 영속 컨텍스트를 삭제한다

  • commit = 실제 트랜잭션을 DB에 반영한다.

트랜잭션이 커밋되는 시점에는 영속성 컨텍스트를 삭제한다.

컨텍스트 삭제란? 진짜 커밋시 삭제되는건가? 그렇다면 엔티티매니저의 close()와의 차이는 뭘까? ... 문의중!

Dirty Checking시 1차 캐시의 엔티티와 스냅샷을 비교할 수 있는 이유 - set()을 호출했을 때 동작

set()으로 데이터를 변경하고, update()나 persist()를 호출하지 않지만 commit시 dirty checking을 통해 DB에 알맞게 데이터 변경이 일어난다고 배웠다.

dirty checking의 과정에서 1차 캐시의 스냅샷과 엔티티를 비교한다고 했는데, 이 엔티티는 1차 캐시에 있는 엔티티를 말한다.

Dirty Checking이 발생하기 전, set()만 했을 때 어떤 일이 발생하길래 1차 캐시의 엔티티와 비교할 수 있는걸까?

과연, set()만으로 변경값이 1차 캐시의 엔티티에 반영되는건가?

그렇다!

  • 💡set()을 하면 1차 캐시에 해당 내용이 바로 반영된다.
    따라서 이후 조회할 때 사용할 수 있게된다! 하지만 당연히 DB에는 아직 동기화가 안되어있다.

더 살펴보면, flush가 발생할 때, 변경감지가 일어나는데 이때 update 쿼리가 생성된다.
아직 DB에 반영되기 전인데, 코드단에서 set()으로 데이터를 변경하고 나서 find()를 사용하면 어떻게될까?
위에서 알아봤듯이, 1차 캐시에는 업데이트 된 엔티티가 관리되고있기 때문에 set으로 수정 의도한 데이터가 잘 불러와진다.

+변경 감지는 영속성 컨테스트가 관리하는 영속 상태의 엔티티에만 적용된다❗️

remove()의 로직도 자세히 살펴 볼 필요가 있다.

  1. 엔티티를 삭제하기 위해서도, 먼저 삭제 대상의 엔티티 조회가 필요하다
    -> em.find()를 실행해서 엔티티가 영속 컨텍스트 내부 캐시에 등록되어 있지 않을 경우, DB를 조회해서 객체를 영속 컨텍스트에 저장
  2. DB에서 바로 삭제되는 것이 아니라, 영속성 컨텍스트(엔티티 매니저)에서만 제거되며, DELETE 쿼리를 쓰기 지연 SQL 저장소에 저장
    -> em.remove()를 실행했을 때, 캐시에 등록되어 있던 엔티티가 삭제하고 delete 쿼리문을 쓰기 지연 SQL 저장소에 저장
  3. 트랜잭션 커밋 시 DB에서 삭제

의문점

  1. 코드상에서 생성한 객체를 persist하지않고, remove()먼저 하면 어떻게 될까? 에러가 발생하나? 무슨에러가 발생하지?
  2. "삭제 대상의 엔티티 조회가 필요하다"의 뜻이 정확히 뭘까?
    내가 remove()를 쓰면 해당 엔티티 자동으로 찾아서 1차 캐시에 등록해주는걸까? 아니면 예외처리 느낌으로 내가 직접 영속 컨텍스트에 있는지 확인해서 없으면 remove()전에 find를 하라는걸까?

위 의문점 2가지에 대해 알아보자.

코드상에서 생성한 객체를 persist하지않고(영속 컨텍스트에서 관리되지 않을때), remove()먼저 하면 어떻게 될까?

이 의문점의 포인트는 사실 영속 컨텍스트이 1차캐시에 없는 엔티티를 remove()하려하면 어떤 일이 일어나는지이다.
예외가 발생하면 어떤 예외가 발생하는지도 궁금하다.

우선 H2 DB를 사용할거고, 현재 Member테이블에 있는 데이터는 아래와같다.

1. DB에 id, name 둘 다 없는 값으로 세팅해서 remove하면 어떤일이 발생할까?

public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        Member member1 = new Member();
        member1.setId(5L);
        member1.setName("Ben");

        System.out.println("before remove");
        em.remove(member1);
        System.out.println("after remove");
		
        tx.commit();
        em.close();
        emf.close();
    }

위와 같은 결과를 보이는데, 예외는 발생하지않지만 아예 delete쿼리가 안 날라간다.

2. id, name 둘 다 DB에 존재하는 데이터로 세팅해서 하면 어떻게 될까?

이번에는 위 코드를 기반으로, member1의 set부분만 아래코드처럼 수정해서 테스트해봤다.

Member member1 = new Member();
member1.setId(2L);
member1.setName("HelloB");

결과를 보니, 오 IllegalArgumentException이 발생했다.
에러 내용을 보니, detach된 인스턴스를 제거하려다가 발생한것같다.
(예외 발생으로 인해, 이후에 after remove도 뜨지않고 프로그램이 강제 종료됐다.)

-> 추측 : 코드에서 세팅한 값이 DB에 있는 데이터값과 일치하면, (id, name : 둘 다 일치해야하는지는 아직 모르겠다.) 일치하는 값이 있을텐데 영속 컨텍스트의 1차 캐시에 없으니 준영속된 엔티티라고 인식을 하나보다.


예상대로 결과도 그대로이다. (데이터가 삭제되지 않았다)

3. 이번에는 id값(pk)은 동일하고, Name만 다르게 해보자.

Member member1 = new Member();
member1.setId(2L);
member1.setName("HelloC");

동일한 예외가 발생했다.

DB의 데이터 역시 삭제되지않았다.

4. id를 다른값으로 하고(DB에 없는값으로), name은 DB에 존재하는 값으로 하면 어떻게될까?

Member member1 = new Member();
member1.setId(3L);
member1.setName("HelloB");

1번 실험과 동일하게 예외는 발생하지않지만 그렇다고 delete 쿼리가 날라가지는 않는다.

5. find()로 DB의 존재하는 엔티티를 찾아 remove() 해보자

public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    EntityManager em = emf.createEntityManager();

    EntityTransaction tx = em.getTransaction();
    tx.begin();

    Member member1 = em.find(Member.class, 2L);
    System.out.println("before remove");
    em.remove(member1);
    System.out.println("after remove");

    tx.commit();
    em.close();
    emf.close();
}

정상적으로 delete 쿼리가 잘 날라가고, DB에서도 해당 데이터가 지워진것을 볼 수 있다.

💡 그리고 1,2,3,4에서의 테스트 결과를 보니, find()없이 remove()만 수행했음에도 1차 캐시에 엔티티가 없으니까 쿼리가 날라갈때 delete쿼리 전에 select가 먼저 그것도 자동으로 날라가는것을 알 수 있다.

+❗️em.remove(member1)를 호출하는 순간 member1은 영속 컨텍스트에서 제거되어 더 이상 수정할 수 없다.

persist()했다가 해당 트랜잭션이 commit되지않고, 롤백되면 부여받았던 id값은 어떻게 될까?

부여된 id가 회수되지 않고, 다음 id를 사용하게 된다.

준영속과 비영속의 차이는 정확히 뭘까? Entity manager를 clear(), close()하면 준영속되는데, 영속 컨텍스트가 비어지고 초기화된다는건 결국 준영속도 비영속이랑 똑같은거아닌가?

  • '비영속(Transient)' 상태와 '준영속(Detached)' 상태 : JPA에서 엔티티의 상태를 나타내는 용어

이 둘의 큰 차이를 알아보자.

비영속(Transient) 상태

JPA에서 엔티티 객체가 영속성 컨텍스트에 속하지 않은 상태이다. 즉, JPA가 엔티티를 관리하지 않는 상태이다.
비영속 상태의 엔티티는 데이터베이스와는 아무런 관련이 없으며, 단순히 Java 객체로만 존재한다. 예를 들어, new 키워드를 사용하여 엔티티 객체를 생성한 후, 데이터베이스에 저장하지 않은 상태가 비영속 상태이다!

준영속(Detached) 상태

JPA에서 엔티티 객체가 이전에 영속성 컨텍스트에 속했지만, 더 이상 영속성 컨텍스트에 속하지 않는 상태를 말한다.
준영속 상태의 엔티티는 영속성 컨텍스트로부터 분리되어, 영속성 컨텍스트의 관리를 받지 않게 된다.
(즉, 과거에 id값을 부여받았었던 적이 있는것)
일반적으로 영속성 컨텍스트에서 관리되던 엔티티를 영속성 컨텍스트를 종료하거나, EntityManager의 detach() 메서드를 호출하여 분리시킬 수 있다.

준영속 상태의 엔티티는 데이터베이스와는 동기화되지 않으며, 영속성 컨텍스트에서의 변경을 추적하거나 자동으로 업데이트하지 않는다. 따라서, 준영속 상태의 엔티티를 변경하더라도 해당 변경은 데이터베이스에 영향을 주지 않는다.

결론

  • 비영속 상태는 JPA가 엔티티를 전혀 관리하지 않는 상태이고, 준영속 상태는 이전에 관리되었던 엔티티지만 더 이상 관리되지 않는 상태라는 점이다.
  • 준영속 상태는 식별자 값을 가지고 있다.
    비영속 상태는 식별자 값이 없을 수도 있지만, 준영속 상태는 이미 한 번 영속상태였으므로 반드시 식별자 값을 가지고 있다.

Entity Manager의 clear()의 역할에서 '초기화'란 무엇이고, close()의 역할에서 '컨텍스트를 닫는다'는건 무엇일까?

clear

clear 메서드는 영속성 컨텍스트의 모든 엔티티를 초기화하는 역할을 한다.

  • 즉, 영속성 컨텍스트에 캐시된 모든 엔티티를 제거하고, 1차 캐시를 비우는 역할을 수행한다.

이후에 다시 엔티티를 사용할 때는 데이터베이스로부터 다시 로딩하게 된다. clear 메서드는 트랜잭션 범위 내에서 사용할 수 있다.

close

close 메서드는 영속성 컨텍스트를 종료하고 관련 리소스를 해제하는 역할을 한다.

  • close 메서드를 호출하면 영속성 컨텍스트는 완전히 닫히고, 관련된 데이터베이스 연결, 캐시 등의 리소스가 해제된다.

영속성 컨텍스트가 닫힌 후에는 엔티티를 로딩하거나 변경할 수 없다.

따라서,

  • clear 메서드는 영속성 컨텍스트의 캐시를 초기화하여 엔티티를 다시 로딩할 때 사용되며, 영속성 컨텍스트는 여전히 활성화된 상태이다.

반면에,

  • close 메서드는 영속성 컨텍스트를 종료하고 관련 리소스를 해제하여 엔티티를 더 이상 사용할 수 없는 상태로 만든다.

clear() 메서드 호출 후에는 영속성 컨텍스트에 있는 엔티티들은 모두 준영속 상태가 되므로, 변경 사항이 있더라도 데이터베이스에 반영되지 않는다.
따라서, ⚡️ clear() 메서드를 호출하면 해당 트랜잭션 내에서 이전에 변경한 내용이 있다면 모두 롤백되는 효과가 있다.

영속성 컨텍스트를 초기화하기 위해 clear() 메서드를 호출하는 경우는 일반적으로 큰 트랜잭션이나 메모리 사용을 최적화하기 위해 사용될 수 있다.
주의할 점은 clear() 메서드 호출 후에는 이전에 영속성 컨텍스트에서 로드한 엔티티들에 대한 변경 내용이 유실되므로, 주의해서 사용해야 한다!

0개의 댓글