ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot - JPA의 영속성 컨텍스트
    Framework & Library/Spring Boot 2021. 12. 2. 13:36

    영속성 컨텍스트의 정의

    개요

    Spring Boot를 사용하고 있는 요즘에는 Spring 프레임워크에 맞춰져 있는 Spring Data JPA를 사용한다. 그런데, Spring Data JPA를 사용하면서 객체에 있는 데이터들이 스스로 변화를 감지하고, 객체의 데이터가 자동으로 변경, 생성, 삭제가 이루어지는데, 이 과정이 어떻게 가능한지 영속성 컨텍스트를 통해 알아보고자 한다.

     

    영속성 컨텍스트란?

    영속성 컨텍스트란, Entity를 영구적으로 저장하는 환경을 말한다. Spring으로 JDBC를 이용하여 개발을 해본 사람들은 DB에 있는 데이터가 어떤 식으로 읽히고 쓰이는 건지 이해할 수 있을 것이다. 간단하게 요약하자면, 다음과 같다.

     

    Connection 생성 - Connection 연결 - Statement 생성 - Statement 실행 - Transaction 커밋 - Connection 연결 해제

     

    그렇다면 ORM에서는 어떻게 처리를 해야 할까? SQL 코드를 사용하지 않고 객체 지향적으로 처리한다고 생각해보면, 우선 객체를 생성하고, 그다음 DB 처리가 이루어져야 한다. 영속성 컨텍스트는 DB 처리가 되기 이전, 객체를 먼저 생성하는 단계 즉, 그 이후의 단계 처리를 위해 존재하는 컨텍스트이다. 영속성 컨텍스트는 논리적인 개념에 속하며, Spring에서는 EntityManager라는 객체를 통해 사용한다.

     

     

    그런데, 영속성 컨텍스트는 Java에서 뭘 사용하느냐에 따라 사용할 수 있는 것이 제한되어 있다. J2SE의 경우 EntityManager와 영속성 컨텍스트가 1:1 관계를 갖지만, Spring과 같은 J2EE의 경우 같은 트랜잭션 번위에 있는 EntityManager는 동일한 영속성 컨텍스트를 사용한다. 즉, Spring IoC 컨테이너의 기능인 의존성 주입처럼 재활용한다는 이야기이다.


    Entity의 Lifecycle

    비영속(new/transient)
    /** 객체를 생성만 한 상태 */
    Person person = new Person();
    person.setName("홍길동");
    person.setAge(20);

     - 영속성 컨텍스트에 들어가기 전 상태이다.

     - 이때는 DB에 아무런 영향이 가지 않는다. 단지 새로운 데이터를 생성하기 위해 객체 지향적인 프로그래밍 방법을 사용한 상태이다.

     

    영속(managed)
    /** 객체를 생성만 한 상태 */
    Person person = new Person();
    person.setName("홍길동");
    person.setAge(20);
    
    EntityManager em = emf.createEntityManager();
    em.getTransaction().begin();
    
    /** 객체를 저장한 상태 */
    em.persist(person);

     - 영속성 컨텍스트에 저장된 상태이다.

     - Entity가 영속성 컨텍스트에 의해 관리되지만, DB에는 아무런 영향이 없다.

     - 컨텍스트에 미리 객체에 대한 정보를 DB 형태로 저장한다.

     - Transaction과 관련된 commit 메서드가 호출될 때 DB에 적용된다.

     

    준영속(detached)
    /** 객체를 영속성 컨텍스트에서 분리한 상태 */
    em.detach(person);

     - 영속성 컨텍스트에서 분리된 상태이다.

     - 더 이상 해당 객체에 대한 변경 사항이 없을 경우에는 EntityManager에서 준영속 상태로 돌려야 한다.

     

    삭제(removed)
    /** 객체를 영속성 컨텍스트에서 삭제한 상태 */
    em.remove(cafe);

     - 영속성 컨텍스트에서 삭제된 상태이다.

     - 객체를 삭제하게 되면 DB에 반영이 되므로 사용에 유의해야 한다.


    영속성 컨텍스트의 사용 이유

    Cache

    영속성 컨텍스트는 Entity를 영구적으로 저장하는 환경이다. 즉, DB에 저장하는 것이 EntityManager를 이용해서 Entity를 영속성 컨텍스트에 저장하는 것이다.

    이러한 영속성 컨텍스트에는 1차 캐시라는 것이 있다. Entity가 DB에 저장되기 전에 사용되는 공간이며, DB에서 데이터를 조회한 후에도 저장된다. 따라서, 한 번 더 같은 Entity를 조회하고자 할 때 빠른 조회 기능을 제공하고 부하를 줄여준다.

     

     

    영속성 컨텍스트는 2차 캐시까지 존재하지만 독립적으로 사용되는 것은 1차 캐시이다. 2차 캐시의 경우 애플리케이션의 전체 Entity에 사용되는 캐시이다. 애플리케이션이 종료될 때까지 2차 캐시는 존재하지만, Entity의 Transaction Thread가 종료되는 경우 1차 캐시는 삭제된다.

    즉, 1차 캐시는 트랜잭션 범위 안에서만 사용되는 짧은 캐시이다.

     

    DB에서 Entity를 조회할 경우

     - 1차 캐시를 먼저 조회한 후, 해당 Entity가 있으면 1차 캐시의 내용을 불러온다.

     - 1차 캐시에서 Entity를 찾지 못한 경우 2차 캐시에서 찾는다. 2차 캐시에도 없다면, DB에서 찾는다.

     - DB에서 조회한 Entity를 1차 캐시와 2차 캐시에 저장한다.

     - 조회한 Entity를 반환한다. 

     

    DB에 Entity를 저장할 경우

     - 객체로 만든 Entity를 영속 상태로 전환하면 1차 캐시와 2차 캐시에 저장된다. 이 경우 DB에는 반영되지 않는다.

     - 1차 캐시에 저장된 Entity는 트랜잭션 커밋 메서드를 통해 DB에 저장된다.

     - 작업이 끝났으면 준영속 상태로 전환된다.

     

    동일성 보장

    Entity를 DB에 저장할 때, 캐시에 한 번 보관을 한다. 이 말은 캐시에 있는 데이터와 실제 DB에 존재하는 데이터가 같으면서 레퍼런스 관계가 된다는 것이다. 즉, 캐시에 보관하고 있는 데이터와 DB에 있는 데이터가 100% 동일하다는 것을 보장받을 수 있게 된다는 것을 의미한다.

     

     

    1차 캐시에 저장되어 있는 데이터가 실제 DB의 데이터가 동일성을 보장하는 과정은 다음과 같다.

     

    트랜잭션 내부에서 persist() 메서드가 호출되면, Entity들은 1차 캐시에 생성되고, 논리적으로 구현되어 있는 write-behind SQL 저장소에 INSERT 쿼리 등의 DML 쿼리를 생성하여 쌓아 놓는다. 최종적으로 commit() 메서드가 호출될 때 write-behind SQL 저장소에 있는 모든 쿼리가 DB로 전달되며, 이때부터 DB에 내용이 반영되는 것이다.

     

    commit() 메서드가 실행될 때 flush() 메서드를 같이 호출한다. flush() 메서드는 write-behind SQL 저장소에 있는 모든 쿼리를 DB에 전송하는 메서드이고, 그 내용을 반영하는 것이 commit() 메서드이다. 실제 트랜잭션 내의 commit() 메서드는 위 2가지 기능을 모두 수행한다는 점을 인지해야 한다.

     

    Dirty Checking

    Entity를 수정해야 할 경우에는 어떻게 할 수 있을까? JPA를 사용한다고 사정하고, 코드를 짜 본다면 아래와 같이 짤 수 있을 것이다.

     

    /** EntityManager 생성 */
    EntityManager em = emf.createEntityManager();
    
    /** 영속 Entity 조회 */
    Person person = em.find(Person.class, 1);
    
    /** 영속 Entity 수정 */
    person.setName("이순신");
    
    /** 트랜잭션 생성 및 커밋 */
    EntityTransaction transaction = em.getTransaction():
    transaction.begin();
    transaction.commit();

    영속성 컨텍스트에 있는 Entity를 수정한 후, 트랜잭션을 생성하고 commit() 메서드를 호출하게 되면 INSERT 쿼리가 호출되어 새로운 레코드가 생성되는 것이 아닌가를 고민하게 된다.

     

    하지만, INSERT가 아닌 UPDATE 쿼리가 잘 실행된다. 그렇다면, 어떻게 Entity가 변경되었는지 감지하는 걸까?

     

     

    그 이유는 JPA 캐시에 있는 스냅샷 때문이다. 1차 캐시에 저장할 때 id와 Entity 그리고 Snapshot이 저장되는데, commit() 또는 flush()가 호출되었을 때 Snapshot을 비교하여 변경 사항이 있을 경우 UPDATE 쿼리를 만들어 주게 된다.

     

    이러한 변경 사항을 감지하는 기술을 Dirty Checking이라고 한다.

     

    그런데, 우리가 단순히 Setter() 메서드를 이용해서 변경을 하게 되면 UPDATE 쿼리를 작동시키는 것은 맞지만, 모든 필드에 대해서 UPDATE 쿼리를 처리하게 한다. 예를 들어, Entity의 필드가 20개 이상이라면 20개 이상의 필드에 대해서 UPDATE 쿼리를 반영하는 것이다.

     

    이렇게 처리를 하게 되면 처리 속도가 길어지고, 디버깅이 어렵기 때문에 변경 사항에 대해서만 UPDATE 쿼리를 작성하도록 설정할 필요가 있다. 해당 방법에는 "@DynamicUpdate" 애너테이션을 이용하는 방법이 있다.


    출처

    https://blog.neonkid.xyz/232?category=814055

    https://blog.neonkid.xyz/233?category=814055

     

    728x90

    댓글

Designed by Tistory.