새소식

xub2의 개발 새발 🐾
💽 Database/JPA

[JPA] 영속성 컨텍스트 : Persistence Context - 1

2025. 9. 12. 14:58

  • -

JPA는 내부적으로 엔티티 매니저 팩토리를 통해 엔티티 매니저를 각 스레드에서 새로 생성하여 사용한다고 했다.

내부적으로, 이 엔티티 매니저가 DB와의 물리적 커넥션을 획득하여 DB에 쿼리를 전송한다.

그렇다면 JPA는 어떻게 동작하기에 이런 기능들을 제공할 수 있는 것일까?

그 해답은 바로 영속성 컨텍스트이다. 이제 차차 알아보자.


영속성 컨텍스트

영속성 컨텍스트는 엔티티를 영구히 저장하는 환경이라는 뜻이다. -> 즉, 엔티티 (데이터)를 관리해주는 일종의 저장환경 객체이다.

이는 눈에 보이지 않는 논리적인 개념이다. 그리고 반드시 이 영속성 컨텍스트는 엔티티 매니저를 통해 접근되어야 한다.

기본적으로 JPA의 영속성 컨텍스트는 엔티티 매니저와 1:1 관계이다. (스프링 사용시 한 애플리케이션 안에서 여러 엔티티 매니저가 하나의 영속성 컨텍스트 공유).

 

엔티티의 라이프 사이클

1. 비영속 (new / transient) : 영속성 컨텍스트와 관계 X (새로운 상태) -> 즉 persist 되기 전

2. 영속 (managed) : 영속성 컨텍스트에 관리되는 상태 -> persist 후

3. 준영속 (detached) : 영속성 컨텍스트에 저장 되었다가 분리 된 상태

4. 삭제 (removed) : 삭제된 상태

 

쉽게 코드로 예시에 주석을 통해 확인 하자.

@Test
    public void createMemberTableTest() {
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = new Member();
            member.setId(1L);
            member.setName("xuv2"); // 여기까진 비영속 상태

            em.persist(member); // 여기까지 와야 영속 상태
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
    }

추가적으로 준영속 상태는 객체는 그대로 남아있는데 영속성 컨텍스트에서 제거만 할 뿐, 실제 객체는 남아있고,

삭제는 객체가 물리적으로 삭제 되어진 상태를 의미한다.

 

영속성 컨텍스트의 이점

1. 1차 캐시 기능 -> 영속성 컨텍스트는 애플리케이션과 DB 사이에 중간 계층으로써 캐시(버퍼) 역할 수행 가능

키 + 값 쌍으로 데이터를 보관 해둘 수 있음 -> persist를 통해 DB에 삽입하기 전 모아두고 한번에 전송하거나, DB에서 해당 데이터를 가져와서 1차 캐시에 저장해두면 조회 할 때 직접 DB를 살필 필요 없음(먼저 1차캐시를 찾아보고 없으면 그때 DB에 접근) -> 이 때 Id값은 PK 값을 사용.

Member member = em.find(Member.class, 1L); // 1차 캐시 조회(값 없음) -> DB 접근 -> 1차 캐시에 저장 후 값 반환
Member member = em.find(Member.class, 1L); // DB에 갈 필요 없이 1차 캐시에서 데이터 가져옴

 

2. 동일성 보장 ->  1차 캐시로 REPEATABLE READ (3 수준) 의 트랜잭션 격리 수준을 DB가 아닌 애플리케이션 차원에서 제공한다.

Member member1 = em.find(Member.class, 1L);
Member member2 = em.find(Member.class, 1L);
// (member1 == member2) == true

 

3. 트랜잭션을 지원하는 쓰기 지연 : JPA는 단순 persist() 만으로 곧바로 DB에 저장하지 않는다 -> 트랜잭션을 커밋하는 순간 1차 캐시에 모아 두었던 데이터에 대한 SQL을 DB에 전송하여 데이터를 저장한다.

이때 쓰기지연 SQL 저장소에 SQL들이 보관된다.

쓰기 지연 저장소에 저장된 SQL들은 트랜잭션 커밋시점에 실제 DB에 전송

 

@Test
public void createMemberTableTest() {
    EntityTransaction tx = em.getTransaction();
    tx.begin();

    try {
        Member member = new Member();
        member.setId(1L);
        member.setName("xuv2"); 

        em.persist(member); // 이거 한다고 아직 쿼리 안나감 -> 1차 캐시에 저장 되어 있는 상태
        tx.commit(); // 커밋이 되어야 실제 SQL이 나감
    } catch (Exception e) {
        tx.rollback();
    } finally {
        em.close();
    }
}

4. 변경 감지

코드를 보자.

@Test
public void updateMemberTest() throws Exception {
    EntityTransaction tx = em.getTransaction();
    tx.begin();

    try{
        Member member = em.find(Member.class, 2L);
        member.setName("호날두");
        
        // persist(), update() 와 같이 저장하는 로직이 필요가 없다

        tx.commit();

    } catch(Exception e) {
        tx.rollback();
    } finally {
        em.close();
    }
}

단순하게, 그냥 자바 코딩하듯이 member을 찾고 해당 member의 데이터를 변경한 뒤 따로 저장하는 로직이 필요가 없다.

그 이유는 JPA는 트랜잭션 커밋시 Dirty Checking (변경 감지) 기능을 제공하기 때문이다.

영속성 컨텍스트는 커밋 + flush() 가 실행되면 엔티티의 현재 상태와 1차 캐시에 최초 저장되어 있던 상태 (스냅샷)을 비교한다.

만약 최초의 상태와 현재 상태가 같다면 따로 쿼리를 날리지 않는다.

만약 현재 엔티티와 엔티티의 스냅샷의 필드 동등성 비교가 하나라도 false가 되면, 그때 UPDATE 문을 만들어 DB에 쿼리를 전송한다.

 

5. 지연 로딩

지연 로딩은 나중에 포스팅할텐데, 쉽게 생각해 해당 엔티티를 조회할 때, 조회 대상 엔티티와 연관된 엔티티들을 곧바로 조회하는 것이 아닌 실제 데이터가 호출되어 질 때 모든 연관 엔티티를 끌고 온다고 생각만 해두자.

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.