Spring&IntelliJ

JPA 내부동작방식

NandaNanda 2024. 1. 2. 17:18

출처: https://www.youtube.com/watch?v=mj7mOMqHM68&list=PLOSNUO27qFbvzGd3yWbHISxHctPRKkctO&index=6

요약: EntityManager와 그 메서드를 통한 영속객체 관리. 영속성 컨택스트(Persistence Context)

무엇보다 중요한 것은 데이터를 DB에서 가져오기전에 해당 데이터를 가져오고,  트랜젝션이 종료되는시점에 DB에 데이터를 저장하기 전에 데이터가 기록되는 중간단계인 영속성 컨텍스트안의 1차 케시라는 공간이 존재한다는 것을 아는 것이 중요합니다.

 

JPA를 사용한다는 것은 JPA에게 SQL 쿼리문을 생성하는 작업을 위임한다는 것인데 그 과정을 JPA가 어떻게 처리하는지 살펴보는 글

순수 JPA에서는 여태까지 공부하면서 봤던 것처럼 EntityManager를 가져오고 EntityManager를 통해 여러작업을 하게 되지만 강의 후반부에서는 SpringData JPA를 사용합니다. 스프링 프레임워크를 사용한다는 의미는 일반적으로 JPA를 썼을때 했던 여러일들을 프레임워크에 위임한다는 의미가 있습니다. 즉 EntityManagerFactory, EntityManager를 어떻게 받아오는지에 대한 절차들은 우리가 신경쓸 대상이 아니게 됩니다. 다만 JPA를 공부하는데 EntityManager는 기본이 되는 것이므로 알고 계시는 것이 좋습니다.

어떤 객체가 영속적으로 관리되어지기 위해서는 EntityManager의 관리를 받아야 합니다. 단순히 @Entity를 붙인다고 해서 그 클래스의 객체가 영속객체가 되는것이 아님. @Entity는 해당 클래스를 영속 클래스로 만들어 주기 위한 어노테이션임.

EntityManager는 관리자, 영속성 컨텍스트(Persistence Context)는 그 관리자가 관리하는 환경, 공간(cache공간)이라고 생각하면됨. 다음문장을 통해 영속성 컨텍스트가 그냥 환경이라고 생각해도되는 이유를 익숙히 할것!

EntityManager에 의해 이후에 DB에 저장되기 위해 지속적으로 관리되어 지도록 persist(object)하면 그 객체는 영속성 컨텍스트에 관리되어지는 대상이 되고 영속객체라고 명명된다. 영속성 컨텍스트에서 관리되는 와중에 트랜잭션의 구성 요소가 되면 transaction이 끝나고 commit되면 DB에 그 결과가 반영되는 객체. 그러한 객체를 영속객체라고 함.

네모박스가 영속성 컨택스트(Persistence Context)

바로 우리가 데이터를 persist한다고 하여 DB에 반영되는 것이 아닙니다. DB에 반영되는 시점은 commit이후임. persist()는 영속성 컨텍스트(Persistence Context)에 해당객체가 등록된다는 의미임.

 

영속성 컨텍스트를 좀더 자세히 알아보자. persist(c1)한 후 commit하지 않고find(c1)을 하면 select쿼리가 DB로 날라가지 않고 영속성 컨텍스트에 존재하는 그c1객체를 반환한다. 만약 영속성 컨텍스트에는 없고 DB에만 존재한다면 find(c1)은 DB로 select쿼리를 날리고 그 결과를 받게 된다. 여기서 주의할 것이 flush(), commit()한다고 해서 영속성 컨텍스트의 1차캐쉬에 있는 영속성 객체들이 사라지는 것은 아님!

영속성 컨텍스트의 관리에서 벗어나는 준영속 상태가 되기위한 메서드로는 em.detach(entity) (특정 엔티티만 준영속 상태로 전환) em.clear() (영속성 컨텍스트를 완전 초기화) em.close(). 이 세 가지만 존재한다

그림과 같이 1차캐쉬는 영속성 컨택스트(Persistence Context)내부에 있는 공간임

 

 

 

 

------------------------------------------------------------------------------------------------------------------------------------------------------------

아래부터는 영속성 컨텍스트에 대해 매우 유익한 글을 인용하려 한다.

출처: https://velog.io/@seongwon97/Spring-Boot-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8Persistence-Context

참고: https://colevelup.tistory.com/22

영속성 컨텍스트란?

  • JPA를 이해하는데 가장 중요한 용어이다.
  • 엔티티를 영구 저장하는 환경이라는 뜻이다.
  • 어플리케이션과 DB사이에서 객체를 보관하는 가상의 DB같은 역할을 한다.
  • 서비스별로 하나의 EntityManager Factory가 존재하며 Entity Manager Factory에서 디비에 접근하는 트랜잭션이 생기고 없어질 때 마다 하나의 Entity Manager가 생성되었다 없어짐. Entity Manager를 통해 영속성 컨텍스트에 접근한다(즉, EntityManager와 영속성 컨텍스트의 관계는 N:1이 가능함!!)
  • EntityManager에 엔티티를 저장하거나 조회하면 EntityManager는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
  • 영속성 컨텍스트는 EntityManager를 생성할 때 만들어지며 EntityManager를 통해 영속성 컨텍스트에 접근하고 관리한다.
  • 다음 코드로 Entity를 영속성 컨텍스트에 저장할 수 있다.
entityManager.persist(entity);
  • Spring에서는 EntityManager를 주입하여 사용하면 같은 트렌잭션의 범위에 있는 EntityManager는 같은 영속성 컨텍스트에 접근한다. (J2EE와 같은 스프링 프레임워크 같은 컨테이너 환경은 Entity Manager와 영속성 컨텍스트의 관계가 N:1이 가능하다. 하지만 J2SE와 같은 환경은 1:1이다.)

 

EntityManager

  • EntityManager는 영속성 컨텍스트 내에서 Entity들을 관리하고 있다.
  • EntityManager는 JPA에서 제공하는 interface로 spring bean으로 등록되어 있어 Autowired로 사용할 수 있다.
@Autowired
private EntityManager entityManager;
  • Query Method, Simple JPA repository는 직접적으로 entityManager를 사용하지 않도록 한번 더 감싸준 것이다.
  • spring jpa에서 제공하지 않는 기능을 사용하거나 특별한 문제가 있어서 별도로 customizing을 해야한다면 entityManager를 직접 받아서 처리한다.
  • EntityManager는 Entity Cache를 갖고 있다.

Entity의 생명주기

비영속(new)

  • 영속성 컨텍스트와 전혀 관계가 없는 상태이다.
  • 엔티티 객체를 생성하였지만 아직 영속성 컨텍스트에 저장하지 않은 상태를 의미한다(이와 관련해서 @Entity 어노테이션을 어떠한 클래스에 붙이면 당장은 해당 클래스의 객체가 영속성 컨텍스트와는 아무 관계가 없다. 하지만 em.find()를 사용할때와 같이 이후에 @Entity어노테이션은 영속객체와 관련을 가지게 된다. 좀 더 자세한 내용은 내 블로그의 @Entity검색)
    //객체만 생성한 비영속상태 
    User user = new User();

영속(managed)

  • 영속성 컨텍스트에 저장된 상태
  • 엔티티가 영속성 컨텍스트에 의해 관리된다.
  • 영속 상태가 되었다고 바로 DB에 값이 저장되지 않고 트렌젝션의 커밋 시점에 영속성 컨텍스트에 있는 정보들을 DB에 쿼리로 날리게 된다.
@Autowired
private EntityManager entityManager;
// Class내에 Autowired로 EntityManager추가

    //객체만 생성한 비영속상태 
    User user = new User();
    
    // 객체를 저장한 영속상태
    entityManager.persist(user);

준영속(detached)

  • 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 엔티티를 준영속 상태로 만드려면 entityManager.detach()를 호출한다.
    // 영속 -> 준영속
    // user엔티티를 영속성 컨텍스트에서 분리하면 준영속 상태가 된다.
    entityManager.detach(user);
    // 영속성 콘텍스트를 비우면 관리되고 있던 엔티티들은 준영속 상태가 된다. (대기 상태에 있는 변경 데이터들도 삭제)
    entityManager.clear();
    // 영속성 콘텍스트를 종료해면 관리되던 엔티티들은 준영속 상태가 된다.
   	entityManager.close();
    
    // 준영속 -> 영속 
    // detach를 하여 준영속상태에 빠진 entity를 merge를 하면 다시 영속 상태가 된다.
    entityManager.merge(user);

준영속 상태의 특징

  • 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않는다.
  • 식별자 값을 가지고 있다.

삭제(removed):

  • 영속성 컨텍스트와 DB에서 해당 엔티티를 삭제하여 삭제된 상태이다.
    // user엔티티를 영속성 컨텍스트와 DB에서 삭제
    entityManager.remove(user);

영속성 컨텍스트의 특징/이점

1차 Cache

  • Save 메서드와 같이 DB변경하는 메서드를 실행하였을 때 바로 DB가 업데이트 되지 않고 영속성 컨텍스트 내부에 있는 캐시를 거쳐서 DB가 업데이트된다. 해당 캐시를 우리는 1차 캐시라고 부른다.
  • 1차 캐시에는 영속 상태의 엔티티를 저장한다.
  • 1차 cache는 Map의 형태로 만들어진다.
  • Map에는 key는 id값, value는 해당 entity값이 들어있다.
  • key값이 id라서 id로 조회를 하게 되면 영속성 context에 있는 1차 cache에 entity가 있는지 확인을 해보고 값이 있다면 DB조회없이 return한다. 만약 값이 없으면 쿼리문으로 조회를 하고 1차 cache에 저장후 return해준다.(잘은 모르겠지만 이과정에서 많은 자원이 절약될것 같다)
    • id가 아닌 다른 값을 이용하여 조회를 하면 1차 cache가 적용되지 않는다.
  • Delete, Update와 같은 작업을 할 때도 JPA 내부적으로는 ID를 통한 조회를 많이 하게 된다.
  • 즉, 하나의 Transactional에서 id값으로 조회하는 데이터들은 1차 cache에 저장을 하여 관리를 함으로써 JPA의 조회 성능이 올라간다.
  • 1차 캐시에서 조회하는 방법
// entityManager.find(엔티티 클래스 타입, 식별자 값);
User findUser = entityManager.find("User.class", "1L");
  • 아래의 코드와 같이 data를 id값으로 반복적으로 찾게 되었을 때 @Transactional이 붙는다면 JPA는 데이터를 조회하며 cache에 저장을 한 후 다음 조회때는 같은 조회를 하지 않고 cache에 있는 값을 내보낸다.
    @Test
    void cacheFindTest() {
        System.out.println(userRepository.findById(1L).get());
        System.out.println(userRepository.findById(1L).get());
        System.out.println(userRepository.findById(1L).get());
    }

위의 실행 결과를 보면 1차 캐시의 적용으로 인해 Select문으로 한번의 조회만 하였지만 3개의 결과가 나오는 것을 확인할 수 있다. 즉 한번의 select SQL쿼리를 사용함. 나머지 두번은 SQL쿼리 날리지 않고 그냥 1차 캐쉬에 있는 내용가지고 온것임.

1차 Cache조회의 흐름
1. 1차 캐시에서 탐색한 ID값의 엔티티를 찾는다.
2-1. 탐색한 결과 해당 엔티티가 1차 캐시에 존재하면 값을 가져온다.
2-2. 탐색 결과가 1차 캐시에 존재하지 않다면 DB에서 값을 조회하고 조회한 데이터를 엔티티로 생성해 1차 캐시에 저장한다. (해당 엔티티를 영속 상태로 만든다.)
3. 조회한 엔티티를 반환한다.

🚨 주의

  • 1차 캐시는 서로 공유하지 않고 하나의 쓰레드가 시작할때부터 끝날때까지 잠깐 사용하는 글로벌하지 않는 캐시이다.
  • 100명 한테 요청 100개 오면, 엔티티 매니저 100개 생기고 1차캐시도 100개 생긴다. 스레드 종료되면, 그때 다 사라진다.
  • 트랜잭션의 범위 안에서만 사용하는 굉장히 짧은 캐시 레이어이다.
  • 전체에서 쓰는 글로벌 캐시는 2차 캐시라고 한다.

 

동일성(Identity) 보장

  • 영속성 콘텍스트는 영속 엔티티의 동일성을 보장한다.
    동일성은 값 뿐만 아니라 실제 인스턴스 자체가 같다는 뜻이다.
  • 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 DB가 아닌 애플리케이션 차원에서 제공한다.(속도 면에서 이득이 있을 거같음)
User findUser1 = entityManager.find("User.class", "1L");
User findUser2 = entityManager.find("User.class", "1L");
System.out.print(findUser1 == findUser2) // Result: true
  • 동일성을 보장하여 Id값이 1인 user를 조회한 두번의 결과의 동일성 비교를 해본 결과 true의 결과가 나오는 것을 확인할 수 있다.

 

트랜잭션을 지원하는 쓰기 지연(transactional write-behind)(위 그림에서 나오는 쓰기지연 SQL저장소)

  • entity값을 변경하면 DB에 바로 업데이트 하지 않는다.
  • 트랜젝션 내부에서 영속 상태의 entity의 값을 변경하면 INSERT SQL Query들은 DB에 바로 보내지않고 쿼리 저장소에 쿼리문들을 생성해서 쌓아둔다.
  • 쿼리 저장소에 쌓여있는 쿼리들은 entityManager의 flush()나 트렌젝션의 commit을 통해 보내지게 된다.
	entityManager.flush();
  • flush()는 1차 캐시를 지우지 않고 쿼리를 DB에 날려서 DB와의 싱크를 맞추는 역할을 한다. 쿼리를 보내고 난 후에 commit()을 실행한다.
  • 트렌젝션을 커밋하면 flush(), commit()을 하게 된다(flush혹은 commit한다고 해서 1차 캐쉬에 있는 영속객체가 사라지지 않음).

(참고. 쓰기지연을 왜 하는 것인가? DB커넥션 시간을 줄일 수 있고, 한 트랜잭션이 테이블에 접근하는 시간을 줄일 수 있다는 장점이 있다. 출처: https://velog.io/@flre_fly/%EC%93%B0%EA%B8%B0-%EC%A7%80%EC%97%B0-%EC%A0%80%EC%9E%A5%EC%86%8C )

변경 감지(Dirty Checking)

  • 엔티티의 수정이 일어나도 개발자는 영속성 컨텍스트에 따로 알려주지 않아도 영속성 컨텍스트가 알아서 변경 사항을 체크해준다. 이것을 Dirty checking이라고 한다. 즉 DB에서 어떤 데이터를 가져와서 변경하고 따로 EntityManager에게 알리지 않아도 commit()만 하면 DB에 반영됨.
  • 어떠한 경로로 persistence context안에 정보가 처음 들어오면 1차 캐시에 entity를 저장할때 스냅샷 필드에 정보를 저장함. 이후에 commit이나 flush를 할 때 해당 entity와 스냅샷을 비교하여(최적화를 통하지만 어찌됐든 각각의 entity를 각각의 스냅샷과 비교함) 변경사항이 있으면 알아서 UPDATE SQL을 만들어서 DB에 전송한다.

Dirty Checking의 흐름
1. 트랙잭션을 커밋하면 entityManager의 내부에서 먼저 플러시가 호출된다.
2. 엔티티와 스냅샷(해당객체가 처음 영속성 컨텍스트에 어찌어찌해서 들어오게 되면 스냅샷으로 저장됨)을 비교하여 변경된 엔티티를 찾는다.
3. 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 저장한다.
4. 쓰기 지연 저장소의 SQL을 플러시한다.
5. 데이터베이스 트랜잭션을 커밋한다.


플러시 (flush)

  • flush는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다. (영속성 컨텍스트를 비우는 것이 아님❗️)
  • 1차 캐시를 지우지 않고 쿼리를 DB에 날려서 DB와의 싱크를 맞추는 역할(동기화)을 한다.
  • flush()를 하거나 트렌젝션의 commit을 하게 된다면 영속성 컨텍스트 내에 있는 쿼리저장소에 쌓여 있던 INSERT, UPDATE, DELETE SQL들이 데이터베이스에 날라간다.
  • flush를 실행하면 Dirty checking을 통해 스냅샷과 비교하여 수정된 entity를 찾고 UPDATE Query를 만들어 쿼리 저장소에 등록한 후 쿼리 저장소에 저장된 모든 쿼리를 DB에 보내어 동기화한다.

영속성 context의 값이 DB에 반영되는 경우
1. flush를 통해 개발자가 직접 반영하는 경우
2. Transaction이 끝나서 해당 query가 commit되는 시점
3. 복잡한 조회 조건에 JPQL query가 실행되는 시점
(사전에 영속성 콘텍스트에 추가한 데이터들이 flush()되지 않아서 DB에 업데이트 되지 않았다면 JPQL쿼리문을 수행하는데 오류가 발생할 수 있기에 JPQL 쿼리문을 실행하기 전에 자동으로 flush()를 호출한다.)

Flush Option

JPQL을 사용하는데 쿼리문에서 사용하는 테이블들이 해당 트렌젝션의 앞부분에서 사용한 테이블들과 연관이 없다면 해당 JPQL쿼리를 수행하기 전에는 flush를 할 필요가 없게된다.
이러한 상황의 경우 커밋할 때만 플러시를 하는 해당 옵션을 사용한다.

하지만 해당 옵션을 변경하여 사용하여도 큰 이점이 없어서 기본 상태로 사용하는 것을 추천한다.

em.setFlushMode(FlushModeType.~~~)
  • FlushModeType.AUTO
    Default값으로 커밋이나 쿼리를 실행할 때 자동으로 Flush가 된다.
  • FlushModeType.COMMIT
    커밋을 할 때만 flush가 된다.