영속성 컨텍스트 관련 정리

    영속성 컨텍스트


    • 영속성 컨텍스트는 '엔티티 영구 저장하는 환경'이라는 뜻으로 사용한다.
    • 영속성 컨텍스트에 저장하는 em.persist(Object)는 사실 DB에 저장한다는 뜻이 아니다. em.persist(Object)를 하면 영속성 컨테스트에 객체가 영속화된다. 영속화는 영속성 컨텍스트에 저장된다는 것이다. 즉, Entity를 영속성 컨텍스트에 저장한다는 뜻이다. 코드로 좀 더 살펴본다.
    Member member = new Member();
    member.setId(100L);
    
    System.out.println("BEFORE PERSIST");
    em.persist(member);
    System.out.println("AFTER PERSIST");
    
    tx.commit();

    위의 코드를 살펴본다. em.persist로 객체를 저장했을 때, 이 때 INSERT QUERY가 나간다면 Member 객체는 DB에 바로 저장되는 것으로 이해할 수 있다. 

    그렇지만 실행 결과를 살펴보면 그렇지 않다. PERSIST 전후로 BEFORE PERSIST / AFTER PERSIST가 출력되게 했다. 만약 em.persist를 하는 것이 DB에 저장하는 것이라고 한다면, 바로 BEFORE / AFTER 사이에 바로 INSERT QUERY가 나가야 한다. 그런데 INSERT QUERY는 AFTER PERSIST 이후에 나가는 것을 확인할 수 있다.

    이는 위에서 말한 것처럼 em.persist(Object)를 실행한 순간에 객체는 영속화 된 것으로 이해를 할 수 있다. DB에 저장이 되는 시점은 트랜잭션이 커밋되는 시점으로 이해할 수 있다. 

     

    엔티티 매니저 ? 영속성 컨텍스트? 


    영속성 컨텍스트는 실제로 물리적인 공간은 아니라고 한다. 그냥 이해하기 쉽도록 눈에 보이지 않는 공간이 생기고, 거기에 우리는 뭔가 상상속으로 저장할 수 있다고 생각하면 될 것 같다. 

    • 영속성 컨텍스트는 논리적인 개념이다. 즉, 물리적으로 할당된 그런 것은 아니라고 한다.
    • 영속성 컨텍스트는 엔티티 매니저를 통해서 접근할 수 있다.

    엔티티 매니저와 영속성 컨텍스트는 실행되는 환경이 J2SE이면 엔티티 매니저와 영속성 컨텍스트는 1:1로 대응된다. 반면 J2EE, SPRING 환경에서는 엔티티 매니저와 영속성 컨텍스는 N:1로 매칭된다. 

     

     

    엔티티의 생명주기


    • 비영속 : 영속성 컨텍스트와 전혀 관계가 없는 상태 (새로 생성됨)
    • 영속 : 영속성 컨텍스트에 저장, 관리되는 상태. 영속화됨.
    • 준영속 : 영속성 컨텍스에 저장되었다가 detached된 상태
    • 삭제 : 영속성 컨텍스트에서 삭제됨. 

    엔티티는 빈과 마찬가지로 생명주기를 가지고 있다. 이 생명주기는 엔티티와 영속성 컨텍스트의 관계가 어떤 것인지에 따라 위처럼 4가지 상태로 나뉘어진다. 위의 상황을 그림으로 살펴보면 아래와 같다. 

     

    비영속 상태

    Member member = new Member();
    member.setId(100L);

    비영속 상태는 영속성 컨텍스트와 전혀 상관이 없는 상태이다. 예를 들면 객체가 새로 생성되어 아무런 행동도 하지 않았을 때로 볼 수 있다. 코드로 살펴본다면, 위 상태로 볼 수 있겠다. 즉, 영속성 컨텍스트와 아무런 관련이 없다.

     

    영속 상태

    em.persist(member);
    • 영속 상태는 엔티티가 엔티티 매니저에 의해서 영속화되어 영속성 컨텍스트에 저장된 상태다. 영속성 컨텍스트에 저장된 상태기 때문에 영속성 컨텍스트의 관리를 받는다고 볼 수 있다. 
    • 앞서 코드에도 살펴봤지만, 영속화 된 상태는 DB에 저장된 상태를 의미하지는 않는다. 단순히 논리적으로 만들어진 공간에 객체가 저장된 상태라고 보면 된다. 

     

    준영속, 삭제

    • 준영속 상태는 영속성 컨텍스트에 영속화되었다가 detach 되었을 때를 의미한다. 영속성 컨텍스트 관점에서는 영속성 컨텍스트에서 객체가 삭제되는 것을 의미한다. 
    • 삭제 상태는 객체를 삭제한 상태를 의미한다. DB에서도 지워진다.
    • 영속성 컨텍스트에서 지워졌기 때문에 영속성 컨텍스트가 지원하는 기능을 사용할 수 없다. (쓰기 지연, 변경감지)
    • 준영속 상태를 만드는 코드는 세 가지다.
      em.detach(entity) : 특정 엔티티만 준영속
      em.clear() : 영속성 컨텍스트를 완전 초기화
      em.close() : 영속성 컨텍스트를 종료.

     

    준영속 확인 코드

    Member findMember = em.find(Member.class, 1L);
    findMember.setName("membermbemrmermer");
    
    em.detach(findMember);
    
    tx.commit();

    위 코드에서는 변경이 일어났기 때문에 영속상태라면 변경감지를 통해서 자동으로 업데이트 쿼리가 나가야한다. 그렇지만 detach를 통해 영속성 컨텍스트에서 findMember를 준영속화 해주었다. 따라서, 영속성 컨텍스트가 제공하는 편의 기능들을 제공받지 못한다.

    실행 결과를 확인해보면, 실제로 객체를 찾기 위한 SELECT 쿼리는 나간 것을 볼 수 있지만 변경감지 후 업데이트 쿼리는 나가지 않는 것을 확인할 수 있다. 

     

     

    영속성 컨텍스트의 이점


    • 1차 캐시처럼 동작
    • 객체 간의 동일성을 보장
    • 트랜잭션을 지원하는 쓰기 지연
    • 변경 감지(Dirty Checking)
    • 지연 로딩(Lazy Loading)

     

    엔티티 조회, 1차 캐시


    • 영속성 컨텍스트 안에는 1차 캐쉬 같은 공간이 있다.
    • 1차 캐쉬 공간은 @Id, 엔티티를 Key, Value 형식으로 저장하는 것으로 이해할 수 있다. 1차 캐쉬 공간에는 PK값을 Key로 해서 Entity 값이 Value에 저장된다. 

     

    영속성 컨텍스트 1차 캐시의 동작

    Member member = new Member();
    member.setName("memberA");
    member.setId(22222L);
    
    em.persist(member);
    
    System.out.println("findMEmber1");
    Member findMember1 = em.find(Member.class, 22222L);
    System.out.println("findMEmber2");
    Member findMember2 = em.find(Member.class, 2L);
    System.out.println("findMEmber3");
    Member findMember3 = em.find(Member.class, 2L);
    
    tx.commit();

    영속성 컨텍스트 1차 캐시의 세부 동작을 살펴보기 위해 코드를 작성했고, 실행결과를 분석해봤다. 코드는 위의 코드를 사용했다.

    1. member 객체는 New해서 만들어지고, em.persist에서 영속화된다. 이 때 1차 캐시에 PK, Entity 형태로 저장된다.
    2. em.find로 PK값이 22222인 객체를 찾는다. 이 때 1차 캐시에 원하는 엔티티가 있기 때문에 Select 쿼리가 나가지 않는다.
    3. em.find로 PK값이 2인 객체를 찾는다. 1차 캐시에는 없기 때문에 Select 쿼리가 나간다. 이 때 영속화 되면서 반환된다.
    4. em.find로 PK값이 2인 객체를 찾는다. 1차 캐쉬에 있으니 바로 반환해준다.
    5. em.persist의 Insert Query가 나간다. 

    위의 결과와 과정을 그림으로 변경한 것이다.

    1. 먼저 같은 PK를 가지는 엔티티를 영속성 컨텍스트의 1차 캐시에서 찾는다.
    2. 1차 캐시에 값이 없기 때문에 DB에 SELECT 쿼리를 보내서 값을 가져온다.
    3. 가져온 값은 1차 캐시에 저장(영속화)된다. 
    4. 영속화 된 객체를 반환해준다. 

     

    위의 코드를 살펴봐서 정리를 하면 다음과 같은 결과가 있는 것을 알 수 있다. 

    1. 영속화되면, 영속성 컨텍스트의 1차 캐시에 저장된다. 
    2. Flush() 될 때, 영속성 컨텍스트의 1차 캐시의 값은 유지된다. Clear() 될 때, 영속성 컨텍스트의 1차 캐시의 값은 사라진다.
    3. DB에서 값을 find로 불러오면, 영속화 된 후 값을 반환한다.
    4. 1차 캐시에 있는 값은 DB까지 가지 않고 1차 캐시에서 바로 RETURN 된다. 

     

    영속성 컨텍스트의 1차 캐시의 장점, 동일성 보장

    1차 캐시의 장점 중 하나는 객체의 동일성을 보장해준다는 점이다. 예를 들어 DB에서 값이 같은지를 확인하기 위해서는 PK값이 같은지를 확인한다. 그런데 이건 객체스럽지 않다. JPA를 사용하면 영속성 컨텍스트의 1차 캐시를 활용해 객체의 참조를 비교해, 같은 객체인지를 봐준다는 것이다. 단, 이것은 동일 트랜잭션에서만 가능하다.

    객체끼리 다른지 비교할 수 있다.

    Member memberA = new Member();
    Member memberB = new Member();
    
    memberA.setName("memberA");
    memberB.setName("memberB");
    memberA.setId(400L);
    memberB.setId(401L);
    
    em.persist(memberA);
    em.persist(memberB);
    
    System.out.println("memberA == memberB : " + memberA.equals(memberB));

     

     위 코드는 서로 다른 PK값을 가지는 객체를 선언하고 같은 값인지를 비교하는 코드이다. 당연히 PK값이 다르기 때문에 DB TABLE에서도 서로 다른 객체이고, 객체의 참조가 다르기 때문에 다른 객체로 볼 수 있다. 실제 실행 결과는 당연하지만 False가 나온다

    객체끼리 같은지 비교할 수 있다. 

    Member findMember1 = em.find(Member.class, 2L);
    Member findMember2 = em.find(Member.class, 2L);
    System.out.println("findMember1 = findMember2 : " + findMember1.equals(findMember2));
    

    위 코드는 동일한 PK로 두 번 각기 다른 객체에 저장했고, 그 객체가 서로 같은 참조를 가지는지를 비교하는 코드이다. 앞서 분석한 코드에서 조회를 하면 SELECT 쿼리를 이용해 1차 캐시에 엔티티가 저장되고, 참조는 엔티티를 가리키게 된다. 따라서 참조는 같은 엔티티를 가리키기 때문에 True가 나온다. 

     

    엔티티 등록 : 트랜잭션을 지원하는 쓰기 지연 


    엔티티를 등록할 때, 영속성 컨텍스트는 트랜잭션에서 쓰기 지연을 적용하게 해준다. 이유는 em.persist(member)를 사용했을 때, 다음과 같이 동작하기 때문이다. 이를 버퍼링(모아서 한방에 보내기)이라고 볼 수 있는데, 만약 persist 할 때 마다 Insert Query가 나가면 최적화를 하는데 도움이 되지 않는다.

    예를 들어서 JDBC Batch를 사용해서 한방에 보낼 수 있다. 나중에 공부할 내용이지만 JDBC Batch를 사용해서 한방에 보낼 수 있다고 한다. 이 때, persistence.xml을 살펴보면 jdbc_batch에 Size만큼 모았다가 한방에 보낼 수도 있다. 

    좌 : em.persist(memberB) / 우 : commit()

    위처럼 동작한다고 한다. 위의 이미지를 참고해서 em.persist(memberA)를 했을 때 실제로 일어나는 동작을 분절화해보면 다음과 같다.

    1. memberA 엔티티가 영속성 컨텍스트의 1차 캐시에 저장됨.
    2. JPA가 memberA를 분석해서 쓰기 지연 SQL 저장소에 쿼리를 쌓는다.
    3. memberB 엔티티를 영속석 컨텍스트의 1차 캐시에 저장됨
    4. JPA가 memberB를 분석해서 쓰기 지연 SQL 저장소에 쿼리를 쌓는다.
    5. tx.commit()을 하면, 쓰기 지연 SQL 저장소에 있던 쿼리들이 Flush() 된다.
    6. Commit이 완료된다.

     

    엔티티 등록, 지연 쓰기 코드로 확인해보기

    Member member1 = new Member();
    Member member2 = new Member();
    
    
    member1.setId(333L);
    member1.setName("A");
    
    member2.setId(334L);
    member2.setName("B");
    
    
    System.out.println("Before Persists");
    em.persist(member1);
    em.persist(member2);
    System.out.println("After Persist");
    
    tx.commit();

    위의 코드로 정말로 PERSIST와 즉시 INSERT 쿼리가 나가는지 안 나가는지를 확인할 수 있다. 실행 결과는 아래에서 확인할 수 있다. BEFORE / AFTER PERSIST 이후에 INSERT 쿼리가 2회 나간다. 위의 이론 부분에서 설명했듯이, PERSIST가 되면 엔티티가 1차 캐시에 저장되고, 그 엔티티에 대한 분석 후 INSERT QUERY가 영속성 컨텍스트 안에 있는 쓰기 지연 SQL에 저장된다. 그리고 트랜잭션이 커밋되기 직전에 Flush() 되어서 쿼리들이 나가게 되고, 마지막으로 Transaction이 Commit 되는 것을 볼 수 있다. 

     

     

    엔티티 수정 : 변경감지로 자동 업데이트(Dirty Check)


    영속성 컨텍스트는 Transaction Commit 시점에 기존에 가지고 있던 엔티티의 값이 변했는지 확인하고, 변했으면 자동으로 업데이트 Query를 작성해서 DB로 쏴준다. 어떻게 해서 이런 일이 발생할까? 아래와 같은 일이 발생하기 때문이다. 요는 값을 불러오는 순간마다 1차 캐시에 그 때의 엔티티 상태를 스냅샷으로 저장해두고, 나중에 비교한다는 것이다. 

    1. tx.Commit()이 실행되기 직전 em.flush()가 실행됨.
    2. em.flush()가 실행되면 JPA는 1차 캐시에 있는 동일한 Id 값을 가지는 Entity와 Entity Snapshot을 비교한다.
    3. 비교한 결과가 차이가 있다고 하면, 쓰기 지연 SQL 저장소에 Update 쿼리를 자동으로 작성해줌.
    4. 쓰기 지연 SQL 저장소 값을 DB로 쓰고 트랜잭션 Commit 한다.
    5. 따라서 직접 Update나 Persist를 사용하지 않더라도 자동으로 Update 쿼리가 나가게 되면서, DB에 자연스레 업데이트가 된다.

     

    자동 업데이트 확인 코드

    Member findMember = em.find(Member.class, 2L);
    findMember.setName("가르시아");         
    
    // em.update(findMember) // 이런 코드가 있어야 할 것 같지만..
    
    tx.commit();

    위의 코드를 실제로 작성해주고, 실행 결과를 확인해본다. 코드 내용은 객체를 DB에서 읽어온 후에, Member의 이름을 바꾸는 것이다. 이 때, DB 입장에서는 업데이트 쿼리를 위한 명령어가 따로 작성되어야 할 것처럼 보인다. 그렇지만 실제로는 그런 코드를 사용하지 않더라도, 자동으로 Update 쿼리가 나가는 것을 볼 수 있다. 

     

    좌 : 멤버 조회 / 우 : 자동 업데이트 쿼리

     

     

    DB의 객체 삭제


    DB에 있는 객체 삭제는 em.remove(객체) 명령어로 사용할 수 있다. 이 명령어는 마찬가지로 쓰기 지연 SQL 저장소에 저장된 후, Transaction Commit 직전에 한꺼번에 실행된다.

     

    DB의 객체 삭제 코드 확인

    Member findMember = em.find(Member.class, 2L);
    System.out.println("BEFORE REMOVE CODE");
    em.remove(findMember);
    System.out.println("AFTER REMOVE CODE");
    Member findMember1 = em.find(Member.class, 100L);
    System.out.println("AFTER FIND CODE");
    
    
    tx.commit();

    위 코드로 실제 어떻게 동작하는지를 살펴본다. 객체가 있는지 확인 한 후에, 그 객체가 있다면 삭제하는 쿼리를 짯다. 그리고 다른 객체를 한번 더 조회하는 쿼리까지 같이 넣었다. 실행 결과를 살펴보자.

    실행결과는 먼저 당연히 객체를 조회하는 것부터 시작한다. 객체를 조회한 후, 삭제는 바로 진행되지 않는 것을 볼 수 있다. 왜냐하면 AFTER FIND CODE 이후에 DELETE 쿼리가 나가기 때문이다. 반면 remove 뒤에 나온 find는 바로 실행되는 것을 확인할 수 있다. 여기서 확인할 수 있는 것은 delete는 쓰기지연 SQL 저장소에 저장되고, select는 하는 즉시 쿼리가 나간다는 것이다. 

     

    영속성 컨텍스의 Flush()


    앞에서부터 계속 Flush()에 대해서 이야기를 했다. 주변 사례를 통해 쉽게 이해를 해보면 배출물이 한껏 쌓인 변기에 물을 내리는 거라고 보면 된다. 쉽게 말하면 영속성 컨텍스트와 DB의 동기화 작업이라고 할 수 있다. 영속성 컨텍스트의 변경점을 계속 쌓아놨다가 이것을 DB에 실제로 반영하는 과정이라고 보면 된다. Flush를 하게 되면 발생하는 일은 다음과 같다.

    • 쓰기 지연 SQL 저장소에 있던 SQL 쿼리가 나간다. (등록, 수정, 삭제 쿼리)
    • 변경감지가 일어나, 필요 시 업데이트 쿼리가 작성된다.
    • 영속성 컨텍스트의 변경내용이 DB에 반영된다.
    • Flush()를 하더라도 영속성 컨텍스트에 있는 1차 캐시는 유지된다. 

    영속성 컨텍스트의 Flush를 실행하는 방법은 무엇이 있을까?

    • em.flush()로 실행
    • tx.commit()할 때 자동으로 em.flush()가 실행됨.
    • JPQL 쿼리 실행 시, Flush()는 자동 호출이 됨.

    JPQL 쿼리를 실행했을 때, 왜 Flush()가 자동으로 실행되는 걸까? 당연하게도 에러를 잡기 위한 것이다.

    Em.persist(memberA);
    Em.persist(memberB);
    Em.persist(memberC);
    
    // 중간에 JPQL 실행
    Query = em.createquery(“select m from Member m”, Member.class);
    List<Member> members = query.getresultList();

    예를 들어 위와 같은 동작을 한다고 가정해보자. 이 경우, 영속성 컨텍스트의 저장될 Member들은 쌓여있다. 그런데 아직 Tx.commit()이 되지 않아서, DB에는 member들은 저장되어있지 않았다. 그런데 이 때 JPQL 쿼리가 실행되게 되면, 어찌됐건 DB에서 한꺼번에 불러와야하는데 현재 상태라면 에러가 날 수 밖에 없다. 이런 에러들을 방지하기 위해 JPQL은 항상 실행 전에 Flush()를 사용해서, 쓰기 지연 SQL 저장소에 있는 내용을 반영한 후에 실행된다. 

     

    또한, Flush()를 사용하는 모드를 em을 통해서 설정할 수 있다. 두 가지 방법이 있는데 커밋이나 쿼리를 실행할 때 마다 플러시, 커밋할 때만 플러시를 할 수 있께 em을 통해서 할 수 있다.

    • em.setFlushMode()를 통해 옵션 설정이 가능하다
    • FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 마다 플러시(기본값)
    • FlushModeType.COMMIT : 커밋을 할 때만 플러시 

     

    영속성 컨텍스트 Flush() 실행 확인.

    Member member = new Member();
    member.setId(1L);
    
    em.persist(member);
    
    em.flush();
    em.clear();
    
    Member findMember = em.find(Member.class, 1L);
    
    
    tx.commit();

    위 코드로 영속성 컨텍스트를 수동으로 Flush()한 결과를 볼 수 있다. persist를 하게 되면 쓰기지연 SQL 저장소에 INSERT Query가 쌓여있는데, 이걸 flush()로 보내버리고 DB에서 다시 SELECT 쿼리로 값을 읽어오는 방법이다.

    실제로 의도한대로 동작하는 것을 확인했다. 만약에 중간에 Flush()를 해주지 않았으면, Insert 쿼리만 1회 나갔을 것이다. 왜냐하면 영속 상태로 1차 캐시에 있을 것이고, em.find를 할 경우 이 값을 return 해주기만 하면 된다. 그리고 Transaction Commit 시점에 em.persist로 만들어진 Insert Query가 쓰기 지연 SQL 저장소에서 Flush()될 것이기 때문이다.

    댓글

    Designed by JB FACTORY