스프링 - 영속성 컨텍스트란?

2024. 8. 13. 15:04스프링

2024/08/13

 

※   영속성 컨텍스트에 대해 알아보자.

   ▶  영속성 컨텍스트란?


           ●  Persistence를 한글로 번역하면 '영속성, 지속성'이라는 뜻이다.

Persistence를 객체의 관점으로 해석해 보자면 객체가 생명(객체가 유지되는 시간)이나 공간(객체의 위치)을 자유롭게 유지하고 이동할수 있는 객체의 성질’을 의미한다.

           ●  좀 더 쉽게 표현해 보자면 Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간이라고 할 수 있다.

 

 

           ●  직접 SQL을 작성하지 않아도 JPA를 사용하여 DB에 데이터를 저장 or 조회할 수 있으며 수정, 삭제 또한 가능

 

 

           ●  이러한 일련의 과정을 효율적으로 처리하기 위해 JPA는 영속성 컨텍스트에 Entity 객체들을 저장하여 관리하면서 DB와 소통한다.

 

   ▶  Entity Manager


           ●  영속성 컨텍스트에 접근하여 Entity 객체들을 조작하기 위해서는 EntityManager가 필요하다.

EntityManager는 이름 그대로 Entity를 관리하는 관리자입니다.

           ●  개발자들은 EntityManager를 사용해서 Entity를 저장하고 조회하고 수정하고 삭제할 수 있다.
           ●  EntityManager는 EntityManagerFactory를 통해 생성하여 사용할 수 있습니다.

 

   ▶  EntityManagerFactory

 

           ●  EntityManagerFactory는 일반적으로 DB 하나에 하나만 생성되어 애플리케이션이 동작하는 동안 사용된다.
           ●  EntityManagerFactory를 만들기 위해서는 DB에 대한 정보를 전달해야 한다.
              ○  정보를 전달하기 위해서는 /resources/META-INF/ 위치에 persistence.xml 파일을 만들어 정보를 넣어두면 된다.

             📌 persistence.xml 파일 예시

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="memo">
        <class>com.sparta.entity.Memo</class>
        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="jakarta.persistence.jdbc.user" value="root"/>
            <property name="jakarta.persistence.jdbc.password" value="{비밀번호}"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/{연결테이블명}"/>

            <property name="hibernate.hbm2ddl.auto" value="create" />

            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
        </properties>
    </persistence-unit>
</persistence>
EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
EntityManager em = emf.createEntityManager();

           ●  EntityManagerFactory emf = Persistence.createEntityManagerFactory("{테이블이름}");
           ●  해당 코드를 호출하면 JPA는 persistence.xml 의 정보를 토대로 EntityManagerFactory를 생성한다.
           ●  EntityManager em = emf.createEntityManager(); 코드를 호출하면 EntityManagerFactory를 사용하여 EntityManager를 생성할 수 있다.

 

   ▶  영속성 컨텍스트의 기능

📌 영속성 컨텍스트는 Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간이다.
      영속성 컨텍스트가 어떻게 Entity 객체를 효율적으로 관리하고 있는지 살펴보자.

 

     ▼  1차 캐시


           ●  영속성 컨텍스트는 내부적으로 캐시 저장소를 가지고 있다.
              ○  우리가 저장하는 Entity 객체들이 1차 캐시 즉, 캐시 저장소에 저장된다.
              ○  캐시 저장소는 Map 자료구조 형태로 되어있다.
                       ✅  key에는 @Id로 매핑한 기본 키 즉, 식별자 값을 저장한다.
                       ✅  value에는 해당 Entity 클래스의 객체를 저장한다.
                       ✅  영속성 컨텍스트는 캐시 저장소 Key에 저장한 식별자값을 사용하여 Entity 객체를 구분하고 관리한다.

 

         🔎  영속성 컨텍스트가 이 캐시 저장소를 어떻게 활용하고 있는지 살펴보자.

           👉 Entity 저장

                   ●  em.persist(memo); 메서드가 호출되면 memo Entity 객체를 캐시 저장소에 저장한다.
             
                        ○  예시) 메모 저장

@Test
@DisplayName("1차 캐시 : Entity 저장")
void test1() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = new Memo();
        memo.setId(1L);
        memo.setUsername("Robbie");
        memo.setContents("1차 캐시 Entity 저장");

        em.persist(memo);

        et.commit();

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}


                        ○  em > persistenceContext > entitiesBykey를 확인해 보면 key-value 형태로 정보가 저장되어있음을 확인할 수 있다.

 

           👉 Entity 조회
                1. 캐시 저장소에 조회하는 Id가 존재하지 않은 경우
                    a. 캐시 저장소 조회

                     b. DB SELECT 조회 후 캐시 저장소에 저장

                   ●  em.find(Memo.class, 1); 호출 시 캐시 저장소를 확인 한 후 해당 값이 없다면?
                   ●  DB에 SELECT 조회 후 해당 값을 캐시 저장소에 저장하고 반환한다.

                        ○  예시) memo 조회 

@Test
@DisplayName("Entity 조회 : 캐시 저장소에 해당하는 Id가 존재하지 않은 경우")
void test2() {
    try {

        Memo memo = em.find(Memo.class, 1);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());


    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
        em.close();
    }

    emf.close();
}

                     ➡️ DB에서 데이터를 조회만 하는 경우에는 데이터의 변경이 발생하는 것이 아니기 때문에
                           트랜잭션이 없어도 조회가 가능하다.
                     ➡️  Memo memo = em.find(Memo.class, 1); 호출 시 캐시 저장소에 해당 값이 존재하지 않기 때문에
                           DB에 SELECT 조회하여 캐시 저장소에 저장한 후 반환한다.

                1. 캐시 저장소에 조회하는 Id가 존재하는 경우

                   ● em.find(Memo.class, 1); 호출 시 캐시 저장소에 식별자 값이 1이면서 Memo Entity 타입인 값이 있는지 조회한다.
                     ○  값이 있다면 해당 Entity 객체를 반환한다.

💡 '1차 캐시' 사용의 장점
    1.  DB 조회 횟수를 줄임 
    2.  '1차 캐시'를 사용해 DB row 1개 당 객체 1개가 사용되는 것을 보장(객체 동일성 보장)
     📌 객체 동일성 보장
@Test
@DisplayName("객체 동일성 보장")
void test4() {
    EntityTransaction et = em.getTransaction();

    et.begin();
    
    try {
        Memo memo3 = new Memo();
        memo3.setId(2L);
        memo3.setUsername("Robbert");
        memo3.setContents("객체 동일성 보장");
        em.persist(memo3);

        Memo memo1 = em.find(Memo.class, 1);
        Memo memo2 = em.find(Memo.class, 1);
        Memo memo  = em.find(Memo.class, 2);

        System.out.println(memo1 == memo2);
        System.out.println(memo1 == memo);

        et.commit();
    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}​

        ●  같은 값을 조회하는 memo1과 memo2는 == 결과 true를 반환한다.
          memo1과 다른 값을 조회하는 memo는 == 결과 false를 반환한다.

 

           👉 Entity 삭제
                1. 삭제할 Entity를 조회한 후 캐시 저장소에 없다면 DB에 조회해서 저장한다.

                 2. em.remove(entity);

                   ● em.remove(memo); 호출 시 삭제할 Entity를 DELETED 상태로 만든 후 트랜잭션 commit 후 Delete SQL이 DB에 요청다.

                        ○  예시) memo 삭제

@Test
@DisplayName("Entity 삭제")
void test5() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = em.find(Memo.class, 2);

        em.remove(memo);

        et.commit();

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

                     ➡️ em.find(Memo.class, 2); 호출되면서 memo Entity 객체를 DELETED 상태로 만들고 트랜잭션 commit 후 Delete SQL이 DB에 요청되었다.

                     ➡️  em.find(Memo.class, 2); 호출 하여 memo 객체를 캐시 저장소에 저장한 후
                     ➡️  entityEntry를 확인해보면 memo Entity 객체가 영속성 컨텍스트가 관리하는 MANAGED 상태인 것을 확인할 수 있다.

                     ➡️  em.remove(memo); 호출 후 memo Entity 객체가 DELETED 상태로 바뀐 것을 확인할 수 있다.
                     ➡️  트랜잭션 commit 후 DB 데이터를 확인해보면 해당 데이터가 삭제 되어있는 것을 확인할 수 있다.

 

     ▼  쓰기 지연 저장소(ActionQueue)

 
         ●  JPA는 트랜잭션처럼 SQL을 모아서 한번에 DB에 반영한다.
              ○  JPA는 이를 구현하기 위해 쓰기 지연 저장소를 만들어 SQL을 모아두고 있다가 트랜잭션 commit 후 한번에 DB에 반영한다.
         ●   Debugging을 통해 실제로 쓰기 지연 저장소에 SQL을 모아서 한번에 반영하는지 확인해보자.

           👉 쓰기 지연 저장소 (ActionQueue)

@Test
@DisplayName("쓰기 지연 저장소 확인")
void test6() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {
        Memo memo = new Memo();
        memo.setId(2L);
        memo.setUsername("Robbert");
        memo.setContents("쓰기 지연 저장소");
        em.persist(memo);

        Memo memo2 = new Memo();
        memo2.setId(3L);
        memo2.setUsername("Bob");
        memo2.setContents("과연 저장을 잘 하고 있을까?");
        em.persist(memo2);

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

 

         ●   쓰기 지연 저장소 확인

              ○  em > actionQueue를 확인해보면 insertions > executables에 Insert할 memo#2, memo#3 Entity 객체 2개가 들어가 있는 것을 확인할 수 있다.

         ●   트랜잭션 commit 후

              ○  actionQueue에 있던 insertions 데이터가 사라진 것을 확인할 수 있다.

              ○  실제로 기록을 확인해보면 트랜잭션 commit 호출 전까지는 SQL 요청이 없다가 트랜잭션 commit 후 한번에 Insert SQL 2개가 순서대로 요청된 것을 확인할 수 있다.

 

     ▼  변경 감지(Dirty Checking)

         ●  영속성 컨텍스트에 저장된 Entity가 변경될 때마다 Update SQL이 쓰기 지연 저장소에 저장된다면❓
              ○  하나의 Update SQL로 처리할 수 있는 상황을 여러번 Update SQL을 요청하게 되기 때문에 비효율적
         ●  그렇다면 JPA는 어떻게 Update를 처리할까❓
              ○  em.update(entity); 같은 메서드를 지원할 것 같지만 찾아볼 수 없다❗

JPA에서는 Update를 어떻게 처리할까?

         ●  JPA는 영속성 컨텍스트에 Entity를 저장할 때 최초 상태(LoadedState)를 저장한다.
              ○  트랜잭션이 commit되고 em.flush(); 가 호출되면 Entity의 현재 상태와 저장한 최초 상태를 비교합.
              ○  변경 내용이 있다면 Update SQL을 생성하여 쓰기 지연 저장소에 저장하고 모든 쓰기지연 저장소의 SQL을 DB에 요청한다.
              ○  마지막으로 DB의 트랜잭션이 commit 되면서 반영된다.
         ●  따라서 변경하고 싶은 데이터가 있다면 먼저 데이터를 조회하고 해당 Entity 객체의 데이터를 변경하면 자동으로 Update SQL이 생성되고 DB에 반영된다.
              ○  이러한 과정을 변경 감지, Dirty Checking이라 부른다.

 

 

 

 

오늘은 영속성 컨텍스트의 개념과 기능에 대해 알아보았습니다.

 


※ 위 이미지들은 스파르타코딩클럽에 저작권이 있으므로 무단 도용 금지 및 상업 목적으로 사용할 수 없습니다.

'스프링' 카테고리의 다른 글

스프링 - Spring Security 프레임워크  (0) 2024.08.14
스프링 - Filter란?  (0) 2024.08.14
스프링 - Entity란?  (0) 2024.08.12
스프링 - Spring Data JPA란 무엇일까?  (0) 2024.08.09
스프링 - IoC Container와 Bean  (0) 2024.08.08