JPA

[JPA] 영속성 컨텍스트

경딩 2024. 11. 16. 01:09

 

jpa 란?

JPA 저장

멤버객체를 만든다고 가정해보자

멤버 객체를 회원 DAO 에 넘기고 회원 DAO 가 JPA 에게 멤버 회원 객체를 저장해줘 라고 던지기만 하면 JPA 가 자동으로 JPA 가 회원 객체를 분석하고 자동으로 INSERT SQL 을 만들어줘도 JDBC API 를 사용해서 db 에 insert 쿼리를 날려준다.

또한 패러다임의 불일치도 해결해준다.

자바 컬렉션의 저장하듯 한줄의 코드로 JPA 에게 회원을 저장시킬 수 있다.

 

이때 회원과 같은 객체를 JPA 에서 엔티티라 부른다. 

엔티티란? DB 테이블에 대응하는 하나의 클래스라 생각하면 된다.

 

 

JPA 조회

 

조회또한 마찬가지로 JPA 가 다 알아서 해준다. EnityObject 를 잘 만들어서 결과로 돌려준다.

 

jpa 에서 가장 중요한 두가지가 있다.

  • 객체와 관계형 데이터 베이스 매핑하기(Object Relational Mapping)
  • 영속성 컨텍스트


영속성 컨텍스트란?

영속성(영원히 지속)은 JPA 를 이해하는 가장 중요한 용어이다.

  • "엔티티를 영구 저장하는 환경" 이라는 뜻이다.
  • 영속성 컨텍스트는 엔티티를 영구 저장하는 환경을 뜻하며 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스같은 역할을 한다.
  • 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트 한개를 만들어 엔티티를 보관하고 관리한다.
em.persist(member); // 엔티티 매니저를 이용해 회원 엔티티를 영속성 컨텍스트에 저장한다는 의미

 

엔티티 매니저?

영속성 컨텍스트는 논리적인 개념으로 눈에 보이지 않는다.

엔티티 매니저를 통해서 영속성 컨텍스트에 접근한다.

  • EntityManager.persist(entity); // 객체를 저장한 상태(영속)
  • persist 명령어를 넣으면 entity 는 영속상태가 된다.
  • 이때 DB 에 쿼리하지는 않는다. 트랜잭션을 쿼리하는 시점에 쿼리가 날라간다.

 

 

 

 

// 객체를 생성한 상태(비영속)

Member member = new Member();

member.setId("member1");

member.setUsername("회원1");

 

영속성 컨텍스트의 동작 원리

https://chaewsscode.tistory.com/200#1%EF%B8%8F%E2%83%A3%C2%A0%EC%98%81%EC%86%8D%EC%84%B1%20%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EB%9E%80%3F-1

member 엔티티를 추가하는 과정

  1. 엔티티가 영속화(persist) 되어 1차 캐시에 저장된다.
  2. 쓰기지연 SQL 저장소에 INSERT 문이 생성되어 1차 캐시에 등록된 데이터를 DB 테이블에 추가할 준비를 한다.
  3. flush 명령 시 쓰기지연 SQL 저장소에 저장되어 있던 쿼리들이 실행되면서 1차 캐시와 DB가 동기화된다.
  4. 마지막으로 commit 까지 완료되면 1차 캐시의 내용이 완전히 DB 에 반영된다.

위 작업은 모두 단일한 트랙잭션 내에서 일어난다.

영속성 컨텍스트란 트랜잭션 내에서 일어나는 작업을 모두 기록하는 공간이며, 모든 작업은 언제든 ROLLBACK 될 수 있다.

 

영속성 컨텍스트 범위

트랜잭션이 같으면, 같은 영속성 컨텍스트를 사용한다.

 

트랜잭션 범위의 영속성 컨텍스트 전략은 다양한 위치에서  엔티티 매니저를 주입받아 사용하더라도, 동일한 트랜잭션 내에서는 항상 같은 영속성 컨텍스트를 사용한다.

@Repository
class Repo1 {
    @PersistenceContext EntityManager em;
    
    public void logic1(){
    	em.xxx(); // Repo1의 영속성 컨텍스트 접근
    }
}


@Repository
class Repo2 {
    @PersistenceContext EntityManager em;
    
    public void logic2(){
    	em.xxx(); // Repo2의 영속성 컨텍스트 접근
    }
}

 

위와 같은 상황에서 아래 logic() 을 실행하는 상황을 예로 들면

@Service
class MainService{
    @Autowired Repo1 repo1;
    @Autowired Repo2 repo2;
    
    @Transctional
    public void logic(){
    	repo1.logic1();
        repo2.logic2(); 
    }
}

 

 

repo1 과 repo2 의 엔티티 매니저는 다르지만, 같은 트랜잭션이기 때문에 같은 영속성 컨텍스트를 사용한다.

그리고 해당 영속성 컨텍스트는 트랜잭션과 생명주기가 동일하다.

 

트랜잭션이 다르면, 다른 영속성 컨텍스트를 사용한다.

스프링 컨테이너는 스레드마다 각각 다른 트랜잭션을 할당하기 때문에 같은 엔티티매니저를 사용해도 다른 영속성 컨텍스트를 사용한다.

따라서 영속성 컨텍스트가 스레드간에 공유되지 않으므로 멀티스레드 상황에 안전하다.

 

 

// 객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

 

 

영속성 컨텍스트는 왜  DB 와 JPA  중간에 있는걸까?

영속성 컨텍스트 이점

  • 1차캐시
  • 동일성 보장
  • 트랜잭션을 지원하는 쓰기 지연
  • 변경감지 
  • 지연 로딩
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        // 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야한다.
        tx.begin();
        // 객체를 생성한 상태(비영속)
        Member member = new Member();
        member.setId("member1");
        member.setUsername("회원1");
        
        // 객체를 저장한 상태(영속)
        em.persist(member);

 

 

1차 캐시 조회

find 문 조회시 1차 캐시에서 우선 조회 후 있을 경우 해당 캐시의 엔티티를 반환하고 없을 경우 DB 에서 조회하여 1차캐시에 저장 후 캐시의 엔티티 값을 반환한다.

            Member member = new Member();
            member.setId("member1");
            member.setName("회원1");
            //1차 캐시에 저장됨
            em.persist(member);
            //1차 캐시에서 조회
            Member findMember = em.find(Member.class, "member1");
            // db 에서 조회 : select 문 날라감
            Member findMember2 = em.find(Member.class, "member2");

영속 엔티티의 동일성 보장

Member a = em.find(Member.class, "member1");  
Member b = em.find(Member.class, "member1");
 System.out.println(a == b); //동일성 비교 true
  • 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공

 

 

엔티티 등록

트랜잭션을 지원하는 쓰기 지연

 

  • JPA 는 persist 문장 실행시 바로 db 에 insert 문을 실행하는 것이 아닌 트랜잭션은 커밋하는 순간에 db 에 insert 문을 실행한다.

 

EntityManager em = emf.createEntityManager();
 EntityTransaction transaction = em.getTransaction();
 //엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
 transaction.begin();  // [트랜잭션] 시작
 
em.persist(memberA);
em.persist(memberB);
 //여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
 
 
 //커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
 transaction.commit(); // [트랜잭션] 커밋

  • persist 시 쓰기 지연 SQl 저장소에 insert 문을 보관하였다가 transaction.commit() 시 jpa 에서 flush 가 동작하면서 실제로 insert 문이 실행된 후 실제 db commit 이 실행됩니다.

 

 

엔티티 수정

변경감지

 

  • 커밋을 하면 내부적으로 flush 가 호출된다. 그 이후 엔티티와 스냅샷을 비교한다.
  • 1차 캐시 내부에는 pk 인 id , 와 엔티티, 스냅샷이 존재한다.
  • 스냅샷은 db 에서 읽어온 값이든 내가 집어넣은 값이든 최초의 상태를 스냅샷으로 찍어둔다.
  • 즉 최초로 영속성 컨텍스트에 들어온 상태를 스냅샷으로 찍어둔다.

커밋 순간 플러시가 되면서 jpa 는 엔티티와 스냅샷을 일일히 다 비교후 값이 다른 부분을 update 쿼리로 생성하여 쓰기 지연 sql 에 저장해둔다.

그리고 쓰기 지연 저장소에서 최종적으로 업데이트가 된다.

 

 

플러시란? 

영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것을 말한다.

즉 영속성 컨텍스트의 있는 쿼리들을 db 에 실행하는 것이다.

 

데이터베이스 트랜잭션이 커밋되면 플러시가 자동으로 발생한다.

플러시 발생시

  • 변경감지
  • 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
  • 쓰기 지연 SQL 저장소의 쿼리를 데이터 베이스에 전송 (등록, 수정 , 삭제 쿼리)

 

영속성 컨텍스트 플러시하는 방법

em.flush() - 직접 호출 // 거의 쓸일이 없지만 테스트 시 필요

트랜잭션 커밋 - 플러시 자동 호출

JPQL 쿼리 실행 - 플러시 자동 호출