JPA/자바 ORM 표준 JPA 프로그래밍 - 기본편

3. 영속성 관리(JPA 내부 구조)_영속성 컨텍스트

luminous_dev 2025. 3. 9. 15:21

JPA에서 가장 중요한 2가지

  • 객체와 관계형 데이터베이스 매핑하기 (Object Relational Mapping)
  • 영속성 컨텍스트 (JPA가 내부에서 대체 어떻게 동작하는 걸까?)

엔티티 매니저 팩토리와 엔티티 매니저 

고객의 요청이 들어올 때마다 엔티티 매니저 생성

엔티티 매니저는 내부적으로 데이터베이스 커넥션을 이용해 DB를 사용하게 됨

 

 

영속성 컨텍스트

  • 엔티티를 영구 저장하는 환경
  • EntityManager.persist(entity);
  • 사실 엔티티를 DB에 저장하는 것이 아닌 영속성 컨텍스트라는 곳에 저장 
  • 엔티티 매니저를 통해 접근할 수 있음 

 

J2SE & J2EE 

 

J2SE 환경 :

엔티티 매니저와 영속성 컨텍스트가 1:1

 

J2EE 환경 : (나중에 이해 가능)

엔티티 매니저와 영속성 컨텍스트가 N:1

스프링 프레임워크 같은 컨테이너 환경 

 

엔티티의 생명주기

 

 

 

1. 비영속

: 객체를 생성한 상태 

 

 

2. 영속 

멤버 객체 생성 > 엔티티 매니저 얻어와서 > 엔티티 매니저에 persist해서 맴버 객체를 넣으면 

> 엔티티 매니저 안에 있는 영속성 컨텍스트에 멤버 객체가 들어가면서 ㅕㅇ속 상태가 됨 

 

 

DB에 저장은 언제될까?

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");//persistenceUnitName을 넘기라고 함 - persistence.xml의  <persistence-unit name="hello"> 부분임
        //이걸 연결하는 순간 데이터베이스랑 연결됨

        //create-entity-manger 꺼내기 - 데이터에서 커넥션 하나 받은 것임
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            //비영속
            Member member= new Member();
            member.setId(100L);
            member.setName("HelloJPA");

            //영속
            em.persist(member);
            System.out.println("=====BEFORE=====");
            System.out.println("member="+member); //쿼리가 안나가고 있음 (DB에 저장이 안되는 것)
            System.out.println("=====AFTER=====");

            //커밋
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

 em.persist(member);에선 쿼리 안 날아감

트랜잭션 커밋하는 시점에 영속성 컨텍스트에 있는 DB 쿼리가 날아가게 됨

 

 

 

준영속, 삭제

준영속 : 회원 엔티티를 영속성 컨텍스트에서 분리 (em.detach(member);)

삭제 : 객체를 삭제한 상태 (em.remove(member);) 

 

영속성 컨텍스트의 이점

1차 캐시 / 동일성 보장 /트랜잭션 지원하는 쓰기 지연 / 변경 감지/ 지연 로딩 

애플리케이션과 데이터베이스 사이에 있음

 

 

 

 

영속성 컨텍스트는 내부에 1차 캐시를 들고 있음

1차 캐시를 영속성 컨텍스트로 이해해도 됨

 

@Id = DB pk로 맵핑한 것 

 Entity 자체 = 값 

 

 

이점)

1. 1차 캐시에서 조회함 

멤버 객체를 저장해놓고 em.find로 조회를 하면  

jpa는 영속성 컨텍스트에서 파인드로 멤버 1이라고 찾으면 먼저 1차 캐시를 뒤짐

멤버 엔티티가 있으면 캐시에 있는 값을 그냥 조회해옴 

 

만약 member2는 DB에는 있는데 1차 캐시에 없으면? 

jpa가 영속성 컨텍스트 안의 1차 캐시에 멤버 2가 없네 >DB에서 조회함 > 멤버 2를 1차 캐시에 저장 > 멤버 2 반환 

 

엔티티 매니저는 데이터베이스 트랜잭션 단위로 만들고 데이터베이스 트랜잭션 끝날 때 같이 종료시켜버림 

고객 요청이 들어와서 비즈니스 끝나면 이 영속성 컨텍스트를 지움, 1차 캐시도 다 지워짐 

 

1차 캐시는 성능의 이점을 크게 얻을 수는 없음

 

- 짧은 순간에만 이득이 있어서 많은 고객이 사용하는 캐시는 아님 

- 데이터베이스 한 트랜잭션 안에서만 효과가 있음 

 

2차 캐시가 애플리케이션 전체에서 공유하는 캐시 

 

JpaMain

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");//persistenceUnitName을 넘기라고 함 - persistence.xml의  <persistence-unit name="hello"> 부분임
        //이걸 연결하는 순간 데이터베이스랑 연결됨

        //create-entity-manger 꺼내기 - 데이터에서 커넥션 하나 받은 것임
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            //비영속
            Member member= new Member();
            member.setId(101L);
            member.setName("HelloJPA");

            //영속
            //1차 캐시에 저장된 것을 가져옴
            //처음 조회할 때는 Select 쿼리 나가야 하지만 두번째 조회에서는 쿼리를 날리면 안됨
            Member findMember1 = em.find(Member.class, 101L); //L : Long 여기서 select 쿼리 안 나감
            Member findMember2 = em.find(Member.class, 101L); //L : Long 여기서 select 쿼리 안 나감

            System.out.println("member="+findMember1.getId());
            System.out.println("member="+findMember1.getName());

            System.out.println("member="+findMember2.getId());
            System.out.println("member="+findMember2.getName());

            //커밋
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

같은 쿼리를 두번 조회할 때 첫번째는 쿼리가 나가지만 두번째는 쿼리가 나가지 않고 객체에서 가져옴 

 

JPA는 엔티티를 조회하면 무조건 영속성 컨텍스트로 다 올림 

 

 

영속 엔티티의 동일성 보장

 

같은 쿼리를 조회하면 같다고 동일성을 보장해줌

 

 

동일한 트랜잭션 안에서 같은 쿼리를 실행하면 동일하다고 뜸 

 

영속성 컨텍스트가 있어서

엔티티를 등록할 때 트랜잭션을 지원하는 쓰기 지연을 쓸 수 있음

트랜잭션 시작하고 > em.persist로 멤버 A, 멤버 B를 넣어둠 

트랜잭션이 커밋하기 전까진 쿼리 날리지 않고 쌓아둠 

 

em.persist(memberA);로 멤버 객체 insert하는 쿼리를 만들고 쓰기 지연 SQL 저장소에 쌓아둠 

이어서 em.persist(memberB);로 멤버 B를 1차 캐시에 넣음 여기서도 insert하는 쿼리를 만들고 쓰기 지연 SQL 저장소에 

 

트랜잭션에서 커밋을 하면 쓰기 지연 SQL 저장소에 있던 쿼리들이 " 플러쉬 "가 되면서 날아감 > 그 다음 커밋됨 

 

 

JPA는 리플렉션을 쓰기 때문에 동적으로 객체를 생성해야함 

그래서 기본 생성자가 있어야 함

 

member

package hellojpa;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
//@Table("USER") : 쿼리가 나가는 테이블 지정
public class Member {
    @Id //PK가 뭔지 알려주는 것 Jakarta Persistence로 넣기
    private Long id;

    //만약 Column이 name이고 db가 username이면 @Column (name="username")으로 지정하면 됨
    // 그럼 인서트 쿼리가 name -> username으로 열 이름이 나감
    private String name;

    //JPA는 리플렉션을 쓰기 때문에 동적으로 객체를 생성해야함
    //그래서 기본 생성자가 있어야 함
    //이거 안 써주면 오류 남!!
    public Member() {

    }

    public Member(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
//@Entity를 넣어야 jpa가 관리하는 것을 알고 관리해야겠다고 생각함

 

JpaMain

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");//persistenceUnitName을 넘기라고 함 - persistence.xml의  <persistence-unit name="hello"> 부분임
        //이걸 연결하는 순간 데이터베이스랑 연결됨

        //create-entity-manger 꺼내기 - 데이터에서 커넥션 하나 받은 것임
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            //비영속
            Member member1= new Member(150L,"A");
            Member member2= new Member(160L,"B");

            em.persist(member1); //영속성 컨텍스트에 엔티티와 쿼리 쌓임
            em.persist(member2);

            System.out.println("==============================");

            //커밋
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

 

왜 쿼리가 커밋되고 나가야 하나?

'버퍼링'이라는 기술을 쓸 수 있음 

만약 쿼리가 계속 올라가면 최적화할 수 있는 여지가 없음

데이터베이스에 데이터를 넣어도 커밋 안하면 말짱 꽝 

 

persistence.xml에 넣은 hibernate.jdbc.batch_size

<?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="hello"> <!--jpa 이름은 뭘로 쓸 것인지-->
        <properties>
            <!-- 필수 속성 -->
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.user" value="sa"/>
            <property name="javax.persistence.jdbc.password" value=""/>
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/><!--데이터베이스 방언 : SQL 표준을 지키지 않는 특정 DB만의 고유 기능-->
<!--            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>-->
<!--            <property name="hibernate.dialect" value="org.hibernate.dialect.Oracle10gDialect"/>-->
            <!-- 옵션 -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
            <property name="hibernate.jdbc.batch_size" value="10"/>
            <!--<property name="hibernate.hbm2ddl.auto" value="create" />-->
        </properties>
    </persistence-unit>
</persistence>

 

hibernate.jdbc.batch_size

이 사이즈만큼 모아서 데이터베이스에 한방에 네트워크로 쿼리 보냄 

 

버퍼링 : 모았다가 DB에 한번에 넣을 수 있음 

성능을 더 먹고 들어갈 수 있음

 

엔티티 수정,  '변경 감지' (더디 체킹) 

 

JpaMain 

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");//persistenceUnitName을 넘기라고 함 - persistence.xml의  <persistence-unit name="hello"> 부분임
        //이걸 연결하는 순간 데이터베이스랑 연결됨

        //create-entity-manger 꺼내기 - 데이터에서 커넥션 하나 받은 것임
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            //영속
            Member member = em.find(Member.class,150L);
            System.out.println(member.getName());

            //ID 150의 이름 바꾸기 (A-> ZZZZZ)
            member.setName("ZZZZZ");
            System.out.println(member.getName());

            //수정에서는 persist 하면 안됨 (em.persist(member); (x))

            System.out.println("==============================");

            //커밋
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

이렇게 실행시키면 select 쿼리가 실행 > update 쿼리 실행 

 

 

스냅샷 : 내가 값을 딱 읽어온 그 시점, 최초 시점의 상태를 / 영속성 컨텍스트에, 1차 캐시 들어온 이 상태를 스냅샷으로 떠둔 것 

 

변경 감지

1. flush() > 2. 엔티티와 스냅샷 비교 > 3. update sql 생성 + 쓰기 지연 저장소에 저장  > 4. flush > 5.commit 

 

 

엔티티 삭제 

find한 것을 삭제 .remove();


flush 

: 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영 

:  트랜잭션이 커밋될 때 플러쉬가 일어남 (플러쉬 > 트랜잭션 커밋) 

:  영속성 컨텍스트의 현재 변경 사양과 그 데이터베이스를 딱 맞추는 작업 

: 쌓여있던 쿼리들을 DB에 날림

 

플러시가 발생하면

: 변경 감지 (더티 체킹) 

: 수정된 엔티티 > 쓰기 지연 SQL 저장소에 등록

: 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송 (등록, 수정, 삭제 쿼리) 

 

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

- em.flush() - 직접 호출  / 이건 잘 쓸일 없음 - 미리 DB 넣고 싶거나 쿼리 보고 싶을 때 사용  

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

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

 

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");//persistenceUnitName을 넘기라고 함 - persistence.xml의  <persistence-unit name="hello"> 부분임
        //이걸 연결하는 순간 데이터베이스랑 연결됨

        //create-entity-manger 꺼내기 - 데이터에서 커넥션 하나 받은 것임
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            //영속
            Member member = new Member(200L,"member200");
            em.persist(member);
            System.out.println("==============================");

            em.flush();// 인서트 쿼리가 먼저 나가고 커밋
            //커밋
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

Q. 플러쉬를 하게 되면 1차 캐시가 다 지워질까? NO

1차 캐시는 그대로 유지됨

쓰기 지연 저장소에 쌓여있던 쿼리를 데이터 베이스에 반영하는 과정일 뿐 

 

JPQL 쿼리 실행 시 플러시가 자동으로 호출되는 이유

persist는 데이터에 저장하는 것이 아니기 때문에 JPQL로 A,B,C를 조회하면 조회가 되지 않을 것임

그래서 JPA는 이것을 방지하고자 쿼리 실행 시에는 무조건 플러시를 날림

 

플러시 모드 옵션

ㄴ 쓸 일 없음

 

FlushModeType.AUTO (이걸 사용하는 것을 권장)

기본적으로 AUTO라고 되어 있음 

커밋이나 쿼리를 실행할 때 플러시 (기본값)

 

FlushModeType.COMMIT

커밋할 때만 플러시 / 쿼리를 실행할때는 플러쉬가 안됨 

 

JPA는 어떤 데이터에 맞추거나 동시성에 대한 것은 데이터베이스 트랜잭션에 위임해서 씀

 

 

준영속 상태 

 

더보기

영속 상태 되는 방법

1. em.persist();

2. DB에는 있는데 1차 캐시에 없을 경우 1차 캐시에 저장하면 '영속' 상태가 됨 



 

JpaMain - detach

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");//persistenceUnitName을 넘기라고 함 - persistence.xml의  <persistence-unit name="hello"> 부분임
        //이걸 연결하는 순간 데이터베이스랑 연결됨

        //create-entity-manger 꺼내기 - 데이터에서 커넥션 하나 받은 것임
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            //영속
            Member member = em.find(Member.class, 150L);
            member.setName("AAAA");

            //커밋 전에 detach해서 셀렉트 쿼리만 나가고 업데이트 쿼리 안 나감
            em.detach(member);

            //커밋
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

JpaMain 

em.clear(); = 엔티티 매니저 안에 있는 영속성 컨텍스트를 통째로 날리는 것 

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");//persistenceUnitName을 넘기라고 함 - persistence.xml의  <persistence-unit name="hello"> 부분임
        //이걸 연결하는 순간 데이터베이스랑 연결됨

        //create-entity-manger 꺼내기 - 데이터에서 커넥션 하나 받은 것임
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            //영속
            Member member = em.find(Member.class, 150L);
            member.setName("AAAA");

			em.clear();
            
            //커밋
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

아무 쿼리 안나감 (select는 나가지만 update는 x)

영속성 컨텍스트를 다 날려서 초기화해서.  

 

그럼 똑같은애를 clear 다음에 호출하면?

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");//persistenceUnitName을 넘기라고 함 - persistence.xml의  <persistence-unit name="hello"> 부분임
        //이걸 연결하는 순간 데이터베이스랑 연결됨

        //create-entity-manger 꺼내기 - 데이터에서 커넥션 하나 받은 것임
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            //영속
            Member member = em.find(Member.class, 150L);
            member.setName("AAAA");

            //커밋 전에 detach해서 셀렉트 쿼리만 나가고 업데이트 쿼리 안 나감
            //em.detach(member);

            em.clear();

            Member member2 = em.find(Member.class, 150L);
            //커밋
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

다시 영속성 컨텍스트를 만들어서 쿼리가 두번 나감

1차 캐시와 관계없이 테스트 케이스 작성하거나 눈으로 보고 싶을 떄 유용 

 

준영속 상태로 만드는 법

1. em.detach(entity); : 특정 엔티티만 준영속 상태로 전환 

2.em.clear(); : 영속성 컨텍스트를 완전히 초기화 

3. em.close(); : 영속성 컨텍스트 종료 

 

트랜잭션 주기와 영속성 컨텍스트를 잘 맞춰야함!