본문 바로가기
Framework/JPA

[JPA] 엔티티(Entity) 생명주기, 1차캐시, 변경 감지(Dirty-Checking)

by 곰민 2023. 1. 30.

JPA에서 엔티티(Entity)의 생명주기와 , 1차 캐시(First-Level-Cache)와 1차 캐시가 갖는 장점들, 변경감지(Dirty-Checking)와 플러시(Flush())가 내부적으로 어떻게 동작하는지에 대해서 알아보도록 하겠습니다.

 

엔티티의 생명 주기


비영속(new/transient)


영속성 컨텍스트와 전혀 관계가 없는 새로운 상태입니다.

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//객체만 생성한 상태 jpa와 아에 관계가 없음

 

영속(managed)


영속성 컨텍스트에 관리되는 상태

영속상태

//상단 세줄은 비영속 상태.
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin(); //db transaction 시작

//객체를 저장한 상태 (영속)
em.persist(member);
//이지점에서 바로 쿼리가 날라가지 않음

//트렌젝션을 커밋하는 시점에 영속성 컨텍스트에 있는게 디비에 쿼리로 날라감.
tx.commit();

 

준영속(detached)


persist(member);

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

위의 코드와 같은 경우 찾아서 없는 경우에는 1차 캐시에 엔티티를 넣어둡니다영속상태로 변경됩니다.

준영속 상태는 영속성 컨텍스트에 저장되었다가 분리된 상태입니다.

당연히 영속성 컨텍스트가 제공하는 기능을 사용할 수 없습니다

 

준영속 상태로 만드는 법

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

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

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

PCMember pcMember2 = em.find(PCMember.class, 150L);
pcMember2.setName("AAAAA");

em.detach(pcMember2); //jpa가 관리를 안함. 영속상태에서 빠짐
tx.commit(); 
Hibernate: 
    select
        pcmember0_.id as id1_1_0_,
        pcmember0_.name as name2_1_0_ 
    from
        PCMember pcmember0_ 
    where
        pcmember0_.id=?

 

select 문만 나가고 update 문은 나가질 않습니다.

 

PCMember pcMember2 = em.find(PCMember.class, 150L);
pcMember2.setName("AAAAA");
em.clear();
PCMember pcMember3 = em.find(PCMember.class, 150L);
tx.commit();
Hibernate: 
    select
        pcmember0_.id as id1_1_0_,
        pcmember0_.name as name2_1_0_ 
    from
        PCMember pcmember0_ 
    where
        pcmember0_.id=?
Hibernate: 
    select
        pcmember0_.id as id1_1_0_,
        pcmember0_.name as name2_1_0_ 
    from
        PCMember pcmember0_ 
    where
        pcmember0_.id=?

 

1차 캐시가 다 날아간 상태이며 캐시값이 날아간 이후 다시 한번 find 하기 때문에 쿼리문이 두 번 나가게 됩니다.

 

삭제(removed)


삭제된 상태 즉 디비 삭제를 요청하는 상태입니다.

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

 

Detached vs Removed

Detached의 경우 영속성 컨텍스트와 분리되어 transaction이 커밋될 때 Detached 된 엔티티에 대한 변경사항은 데이터베이스에 적용되지 않지만 Detached된 경우 삭제된 경우가 아니기 때문에 추후 영속성 컨텍스트와 연결되어 데이터베이스에 변경사항이 커밋될 수도 있습니다.
하지만 Entity가 Remove 돼버리면 데이터베이스에서 삭제가 됩니다.
Entity는 영속성 컨텍스트의 영속상태에서 벗어나게 됩니다.
이후 추가적인 작업들에서 Exception을 초래할 수 있습니다.

 

엔티티 생명주기 Flow

출처:자바 ORM 표준 JPA 프로그래밍

 

영속성 컨택스트에서의 1차 캐시(First Level Cache)는 무엇일까요?


JPA 1차 캐시(First Level Cache)는 JPA의 cache 메커니즘입니다.

1차 캐시는 영속성 컨텍스트의 구성 요소입니다.

 

//상단 세줄은 비영속 상태.
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

//엔티티를 영속
em.persist(member); //em -> 엔티티 매니저

 

1차 캐시

@Id Entity
"member1" member(객체)
setId("member1") Member member = new Member();

DB pk로 매핑한 것이 key가 되고 Value Entity 객체 자체가 됩니다.

 

1차캐시 안에 있는 경우

 

1차 캐시 안에 존재할 시 조회 시 Select Query가 나가지 않습니다.

PCMember pcmember = PCMember.withIdName(101L, "HelloJPA"); //static factory
em.persist(pcmember); //em -> entity manager
PCMember findPCMember = em.find(PCMember.class, 101L); //PCMember -> class
findPCMember.getId() = 101
findPCMember.getName() = HelloJPA
Hibernate: 
    /* insert PersistenceContextMain.PCMember
        */ insert 
        into
            PCMember
            (name, id) 
        values
            (?, ?)

 

1차 캐시 안에 없는 경우에는 당연히 DB에서 조회한뒤 캐시에 저장합니다.
이후 재조회를 할시에 캐시에서 가져옵니다.

1차캐시 안에 없는 경우

EntityManager em = emf.createEntityManager(); // 재실행 영속성 컨텍스트 다시 생성됨.
....

//위에서 디비에 값을 저장했다고 가정하고 두가지를 읽어 올때.
PCMember findPCMember1 = em.find(PCMember.class, 101L); //처음은 쿼리가 나가야됨
PCMember findPCMember2 = em.find(PCMember.class, 101L); //두번쨰는 나가면 안됨

 

EntityManager는 Transaction 단위로 생성되며 일반적으로 transaction이 끝나면 종료합니다.

 

1차 캐시(First-Level-Cache)를 활용하는 이점


영속된 엔티티의 동일성(identity) 보장


PCMember findPCMember1 = em.find(PCMember.class, 101L); 
PCMember findPCMember2 = em.find(PCMember.class, 101L);
//두가지를 == 비교하면 어떻게 될까?

 

결과는 true가 나옵니다.

EntityManager는 1차 캐시를 활용하여 2번째 줄에서 1차 캐시네 이미 존재하는 엔티티 인스턴스를 반환하기 때문에 true값이 나옵니다.

 

동일한 transaction 내에서 동일한 엔티티가 다시 로드되면 데이터베이스가 아닌 1차 캐시에서 반환되며 다른 transaction이 데이터를 수정하더라도 동일한 transaction 내에서 항상 동일한 데이터가 반환됩니다.
1차 캐시는 transaction 기간 동안 데이터의 일관성을 유지해 주며
1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌
애플리케이션 차원에서 제공할 수 있게 됩니다.

 

transaction을 지원하는 쓰기 지연


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

em.persist(memberA); //A지점
em.persist(memberB); //B지점
//여기까지 INSERT SQL을 데이터베이스에 보내지 않음.

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

 

A 지점에서의 1차 캐시

@id Entity
"memeberA" memberA(객체)

1차 캐시에 들어감과 동시에 JPA가 Entity를 확인하고 insert query를 생성한 뒤 쓰기 지연 sql 저장소에 넣어둡니다.
아직 DB에 커밋하지 않습니다.

 

B 지점에서의 1차 캐시

@id Entity
"memberA" memberA(객체)
"memeberB' memberB(객체)

1차 캐시에 memberB를 넣고 insert query를 쓰기 지연 sql 저장소에 넣어둡니다.

 

마지막 transaction.commit(); 시점에 쓰기 지연 sql 저장소에 있던 insert query가 DB로 전달돼서 넘어갑니다.
데이터베이스와의 왕복 횟수를 줄일 수 있어 성능을 향상할 수 있습니다.

 

그렇다면 쓰기 지연의 단점은 없을까요?

transaction이 커밋되기 전에 application이 다운되거나 오류가 발생한다면
1차 캐시에 저장된 변경 내용이 손실됩니다.
변경사항이 DB에 즉시 기록되지 않기 때문에 Error를 진단하고 데이터의 손상에 대해서
원인을 파악하기 어려울 수 있습니다.

 

persistence.xml에서 hibernate.jdbc.batch_size value = 10이라는 옵션이 있습니다.
이 사이즈만큼 모아서 DB에 한방에 쿼리를 보내서 저장을 시킵니다.

위와 같은 옵션을 잘 활용해야 합니다.

 

변경 감지(Dirty Checking)


Member member = new Member(150L, "A");

em.persist(member1);
transaction.commit();
Member member = em.find(Member.class, 150L);
member.setName("ZZZZZ")
//em.persist(member);

 

query상으로 select 이후 update query까지 실행 되게 됩니다.

출처 : 자바 ORM 표준 JPA 프로그래밍

transaction.commit() 시점에서 flush()를 호출합니다
최초로 영속성 컨텍스트에 들어온 시점의 값을 스냅샷으로 떠둔 뒤 엔티티와 스냅샷을 비교합니다.

비교 후 엔티티의 값이 변경되었다면 update 쿼리를 쓰기 지연 sql 저장소에 넣어 두고 commit시 반영 합니다.

EntityManager.remove() 삭제하는 경우에도 메커니즘 자체는 같습니다.

즉 값을 바꾸면 transaction이 커밋되는 시점에 DB에 변경된 값을 업데이트해줍니다.

 

 

플러시 flush()


그렇다면 flush()는 정확히 어떤 동작일까요?

flush()는 영속성 컨텍스트의 변경사항과 데이터베이스를 맞추는 과정, 즉 동기화를 한다고 보면 됩니다.
flush()가 호출되면 영속성 컨텍스트에서 엔티티에 대한 변경사항들을 확인합니다.
변경사항이 있는 엔티티가 존재한다면 쓰기 지연 SQL 저장소에 query를 등록합니다.
쓰기 지연 저장소에 있는 query를 데이터베이스에 전송합니다.

 

EntityManager.flush() - 직접 호출

transaction.commit() - flush 자동 호출

JPQL Query 실행 - flush 자동 호출

 

PCMember pcmember1 = PCMember.withIdName(200L, "C"); //새로운 값
entityManager.persist(pcmember1);
entityManager.flush(); //즉시 실행 insert 문
System.out.println("================");
transaction.commit();
Hibernate: 
    /* insert PersistenceContextMain.PCMember
        */ insert 
        into
            PCMember
            (name, id) 
        values
            (?, ?)
================

 

"================"가 출력되는 것을 볼 수 있으며
transaction.commit() 이전에 바로 insert Query가 나가게 됩니다

 

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//이지점까지는 쿼리가 1도 안날라간 상태
//중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();

 

JQPL은 default로 flush()를 날려버립니다.

 

 

flush mode option

EntityManager.setFlushMode(FlushModeType.COMMIT)

 

FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 flush 동작시킵니다.

FlushMOdeType.COMMIT : 커밋하는 경우에만 flush를 동작시키고 쿼리 실행 시는 하지 않습니다.

 

참조


자바 ORM 표준 JPA 프로그래밍

baeldung-entity-lifecycle

반응형

댓글