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

3) jpa 구조, 영속성 컨텍스트,영속,준영속,쓰기지연

backend dev 2024. 5. 20.

jpa구조

 

JPA에서 가장 중요한 2가지

 

1. 객체와 관계형 데이터베이스 매핑하기
(Object Relational Mapping)


2. 영속성 컨텍스트

 

 

 

 

고객의 요청이 올때마다 EntityManager를 생성한다.

EntityManager는 내부적으로 data connection을 사용해서 db를 사용하게 된다.

 

 

영속성 컨텍스트

 

• JPA를 이해하는데 가장 중요한 용어


• “엔티티를 영구 저장하는 환경”이라는 뜻

[컨텍스트는 문맥, 환경 이런뜻을 가진다.]


• EntityManager.persist(entity);

[db에 저장한다기보다는 영속성 컨텍스트라는곳에 저장한다는것이다.]

 

 

 

엔티티 매니저?
영속성 컨텍스트?

 

영속성 컨텍스트는 논리적인 개념


• 눈에 보이지 않는다.


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

 

 

 

엔티티 매니저를 생성하면 그안에 영속성 컨텍스트가 존재한다.

 

나중에 스프링 프레임워크까지 하고난뒤 설명.

 

 

 

엔티티의 생명주기

비영속 (new/transient)
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태


• 영속 (managed)
영속성 컨텍스트에 관리되는 상태 -> EntityManager.persist(member)와 같이 persist를 진행한 뒤 상태.


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


 삭제 (removed)
삭제된 상태

 

 

참고

 

 

 

비영속

엔티티매니저 안에는 영속성 컨텍스트가 존재

 

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

 

jpa와 관계없이 객체만 생성한 상태 == 비영속

 

 

영속

 

 

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

Member member = new Member();
member.setId("member1");
member.setUsername(“회원1”);
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();


//객체를 저장한 상태(영속)

em.persist(member);

 

객체를 생성한후

엔티티매니저에 persist를 통해 멤버객체를 집어넣었기때문에

엔티티매니저 내부에 영속성 컨텍스트안에 멤버객체가 들어가면서 영속상태가된다.

 

 

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

    //영속 -> db에 저장되는것이 아닌 영속성 컨텍스트에 들어가는것.
    System.out.println("=====BEFORE======");
    em.persist(member);
    System.out.println("=====AFTER======"); // persist가 sql를 만든다면 이 로그 사이에 존재할것이다.
    tx.commit(); // 커밋
}catch (Exception e) {
    tx.rollback();
}finally {
    em.close();
}
emf.close();

 

persist가 sql를 생성하는것이 아니라는것의 증명

 

tx.commit(); // 커밋하는 시점에서 영속되어있는것에 관한 쿼리가 날아간다. 

 

 

 

준영속, 삭제

//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태 // 영속성 컨텍스트에해서 해당 객체를 지운다.
em.detach(member); 


//객체를 삭제한 상태(삭제) // DB에서 데이터를 지우겠다는 의미
em.remove(member);

 

 

 

영속성 컨텍스트의 이점

• 1차 캐시


• 동일성(identity) 보장


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

(transactional write-behind)


• 변경 감지(Dirty Checking)


• 지연 로딩(Lazy Loading)

 

 

 

엔티티 조회, 1차 캐시

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

//엔티티를 영속
em.persist(member);

키는 설정한 기본키, 값은 객체

엔티티를 영속시키면 1차캐시에 저장된다.

 

 

1차 캐시에서 조회

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

//1차 캐시에 저장됨
em.persist(member);

//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");

find를 하게되면 먼저 1차캐시에서 조회를 진행한다.

 

 

 

데이터베이스에서 조회

Member findMember2 = em.find(Member.class, "member2");

member2는 db에는 있고 1차캐시에는 없다고 가정

 

DB에서 조회해서 1차캐시에 저장하고 반환한다.

이후 다시 멤버2를 조회하게 되면 1차캐시에 저장된 멤버2가 반환될것이다.

 

엔티티매니저는 트랜잭션 단위로 생성되고 트랜잭션이 종료될때 같이 종료된다.

즉 하나의 비즈니스 로직이 끝나면 엔티티매니저(영속성컨텍스트)가 사라진다. == 1차캐시가 사라진다.

 

1차캐시는 좁은범위에서 공유되는 캐시이기때문에 큰 이득은 없고 한 트랜잭션 안에서만 이점이 존재한다.

2차캐시라는 어플리케이션 전체에서 공유되는 캐시라는것도 있다.

 

select는 쓰기지연과 상관없이 바로 실행된다.

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

            //영속 -> db에 저장되는것이 아닌 영속성 컨텍스트에 들어가는것.
            System.out.println("=====BEFORE======");
            em.persist(member);
            System.out.println("=====AFTER======"); // persist가 sql를 만든다면 이 로그 사이에 존재할것이다.


            Member findMember = em.find(Member.class, 101L);
            System.out.println("findMember = " + findMember);


            tx.commit(); // 커밋하는 시점에서 영속되어있는것에 관한 쿼리가 날아간다.
        }catch (Exception e) {
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();

em.persist로 인해 영속성컨텍스트에 들어가고

em.find를 통해 영속성컨텍스트안의 1차캐시에서 해당 멤버를 찾는다.

 

그러므로 따로 select sql문은 생성되지않았고,

persist에 해당하는 insert 쿼리문은 commit으로 인해 생성된다.

 

영속 엔티티의 동일성 보장

    try {
        Member findMember1 = em.find(Member.class, 101L); // 1차캐시에 들어있지않으므로 select sql 문이 실행된다. 그리고 DB 에서 찾아서 1차캐시에서 저장한다.
        Member findMember2 = em.find(Member.class, 101L); // 위의 find 때문에 1차캐시에 해당 멤버가 저장되었으므로, select sql 문은 실행되지않는다.
        System.out.println("findMember = " + findMember1);
        System.out.println("findMember = " + findMember2);
        System.out.println(findMember1==findMember2);

        tx.commit(); // 커밋하는 시점에서 영속되어있는것에 관한 쿼리가 날아간다.
    }catch (Exception e) {
        tx.rollback();
    }finally {
        em.close();
    }
    emf.close();

 

sql 1번만 실행된것을 확인 가능.

 

1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을

데이터베이스가 아닌 애플리케이션 차원에서 제공

 

 

엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연

slect는 쓰기 지연과 상관없이 바로 실행된다.

update,insert,delete가 쓰기지연이 발생한다.

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.

transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋

 

em.persist(memberA)가 실행되면

1차캐시에 멤버A가 저장되고 또한 쓰기 지연 SQL 저장소라는곳에 멤버A를 생성하는 sql또한 저장된다.

 

em.persist(memberB) 또한 똑같이 진행되고

 

커밋 명령문이 실행되면 쓰기 지연 sql 저장소에서 flush가 진행되며

쓰기 지연 SQL 저장소 안에 있던 sql문을 실제 db에서 실행시키고 커밋시킨다.

 

@Entity // jpa 가 관리해야하는 객체라는것을 알리는 어노테이션
@Getter
@Setter
@ToString
@AllArgsConstructor
// jpa 는 내부에서 리플렉션 같은것을 사용하기위해서는 기본 생성자가 필수로 필요하다. 
// 자바에서는 따로설정해둔 생성자가 없으면 기본생성자를 만들어주지만 생성자를 만들었다면 기본 생성자를 추가해준다.
@NoArgsConstructor 
public class Member {

    @Id // pk가 어떤 컬럼인지 알려줘야한다. == 데이터베이스 PK와 매핑해야줘야한다.
    private Long id;
    private String name;
}

 

멤버에 생성자 추가 ( 기본 생성자는 엔티티에 필수)

 

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();

 

커밋때 sql문이 실행되는것을 확인할 수 있다 ==> 트랜잭션을 지원하는 쓰기 지연

 

 

엔티티 수정 - 변경 감지

 

자바에서 컬렉션을 사용하는것처럼 객체를 다루기 위해 jpa를 사용하는것이다.

그러므로 값을 변경후에도 따로 진행해줄게 없다. ( 커밋말고는)

 

    try {
        Member findMember = em.find(Member.class, 150L);
        findMember.setName("ZZZZZ");

        System.out.println("==========");
        tx.commit(); // 커밋하는 시점에서 영속되어있는것에 관한 쿼리가 날아간다.
    }catch (Exception e) {
        tx.rollback();
    }finally {
        em.close();
    }
    emf.close();

 

em.find 첫실행이므로 1차캐시에 해당 멤버가 존재하지않았으므로 select sql문 실행 ,

set으로 jpa에서 관리하던 객체에 데이터 변화가 있으므로 update 생성 및 실행

 

커밋을 하게되면 내부적으로 flush 진행

1차캐시안에는 스냅샷이라는것도 있는데 스냅샷은 값을 조회해온 최초 시점의 상태를 스냅샷으로 저장해놓는것이다. 

플러시가 진행되면 jpa가 엔티티와 스냅샷을 비교해서 데이터의 변경을 확인한다.

변경이 확인되면 update sql를 쓰기 지연 sql 저장소에 저장하고 다시 flush 해서 db에서 실제 sql실행 및 커밋

== 변경감지 (Dirty Checking)

 

엔티티 삭제

//삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, “memberA");

em.remove(memberA); //엔티티 삭제

 

삭제는 조회해와서 삭제하면된다.

( 자바 컬렉션 이용하듯이 )

[ 이 또한 커밋시 delete 쿼리가 나간다. ]

 

jpa는 객체의 변경이 있을시 커밋할때 반영된다.

 

 


 

플러시

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

 

 

플러시 발생

• 변경 감지 [ == dirty checking]

 

• 수정된 엔티티에 대한 sql를   쓰기 지연 SQL 저장소에 등록

 

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

 

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

 

 

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

 

•  em.flush() - 직접 호출


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


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

try {
    Member member = new Member(200L, "member200");
    em.persist(member);

    em.flush(); // 플러시 샐행

    System.out.println("==========");
    tx.commit(); // 커밋하는 시점에서 영속되어있는것에 관한 쿼리가 날아간다.
}catch (Exception e) {
    tx.rollback();
}finally {
    em.close();
}
emf.close();

 

em.flush를 통해 플러시를 직접 호출 가능하다.

 

플러시는 1차캐시를 지우지않는다.

오직 쓰기 영속성 컨텍스트 안의 지연 sql 저장소에 있는 sql를 데이터베이스에 적용하는일을 한다.

 

 

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

 

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// 3개의 sql 문 쓰기 지연 sql 저장소에 저장된다.
// 그리고 1차캐시에 memberA,B,C가 저장된다.

//중간에 JPQL는 바로 실행 된다.  JPQL은 실행전 flush를 먼저 한다. 
query = em.createQuery("select m from Member m", Member.class);


List<Member> members= query.getResultList();

 

플러시 모드 옵션

FlushModeType.AUTO  -> 커밋이나 쿼리를 실행할 때 플러시(기본값)

 

FlushModeType.COMMIT  -> 커밋할 때만 플러시

 

플러시 모드 옵션이 AUTO로 되어있으므로

jpql 쿼리 실행시 자동으로 플러시가 되어 쿼리가 실행되는것이다.

 

 

플러시는

• 영속성 컨텍스트를 비우지 않음


• 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화


• 트랜잭션이라는 작업 단위가 중요 -> 커밋 직전에만 동기화하면 됨 [ 커밋 직전에만 SQL를 날려주면된다.]

 

 


 

준영속 상태

 

 

영속 상태: em.persist를 하거나, em.find에했는데 1차캐시에 없어서 DB에서 찾아서 1차캐시에 넣은상태

즉 1차캐시에 들어있으면 영속상태

 

 

준영속 상태

 

- 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)


- 영속성 컨텍스트가 제공하는 기능을 사용 못함 [ 쓰기지연 , 변경감지[더티체크] 등 ]

 

 

준영속 상태로 만드는 방법

 

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

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

em.close()
영속성 컨텍스트를 종료

 

 

 

 

try {
    Member member = em.find(Member.class, 150L); // 해당 객체가 1차캐시에 없다면 DB에서 가져와서 1차캐시에 저장
    member.setName("aaa"); // jpa 가 관리하는 객체는 변경감지(더티체크)를 하게된다.

    em.detach(member); // 영속성 컨텍스트를 비운다.

    System.out.println("==========");
    tx.commit(); // 커밋하는 시점에서 영속되어있는것에 관한 쿼리가 날아간다.
}catch (Exception e) {
    tx.rollback();
}finally {
    em.close();
}
emf.close();

 

find로 인해 select가 동작하고 해당 객체가 영속성 컨텍스트에 관리되기 시작한다.

[ select 문은 쓰기지연없이 바로 실행된다. ]

그 후 객체.set~ 으로 객체에 대한 변경이 발생하고, 객체는 jpa가 관리하는것이기 때문에 영속성 컨텍스트에 관련된 정보가 있을텐데  그 후 , commit으로 가면 스냅샷으로 변경감지를 하여 update 문이 실행될텐데

 

detach로 인해 해당 객체 관련한것들을 영속성 컨텍스트에서 분리시켜버렸기에

update문은 실행되지않는다.

 

 

 

 

 


정리

 

엔티티매니저를 생성하면 그 안에 영속성 컨테이너가 존재한다.

[ J2SE환경에서 1:1 로 존재한다. -> 현재는 1:1 인것만 설명했음 ]

 

 

영속성 컨테이너에서 관리되면 영속 상태가 된다.

즉 1차 캐시안에서 관리되면 영속상태이다.

 

find를 해서 DB에서 가져와서 1차캐시에 저장하거나

persist를해서 1차캐시에 넣고, 쓰기 지연 SQL 저장소에 넣거나 그럴때 1차캐시에 저장되어 관리되는것을 영속상태라고한다.

 

 

set~를 해서 객체 데이터에 변경을 준후 commit이 실행될때 == flush가 실행될때
1차캐시안에 스냅샷을 이용하여 변경감지를 하고 변경이있다면 쓰기지연sql저장소에 update문이 생기고
다시 저장소에서 flush를해서 DB에 실제 sql이 실행된다.

 

준영속상태 == > 영속성컨테이너에서 관리하고있지않은상태

 

 

 

 

 

 

댓글