BACK/JPA

[JPA] TIL 5일차-영속성 관리(1차 캐시, 쓰기 지연, 변경 감지)

연듀 2024. 4. 1. 20:23

 

영속성 컨텍스트

 

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

Entity Manager는 내부적으로 데이터베이스 커넥션을 사용해 DB를 사용하게 된다.

엔티티매니저를 생성하면 그 안에 1:1로 눈에 보이지 않는 영속성 컨텍스트란 공간이 생성된다.

 

영속성 컨텍스트는 엔티티를 영구 저장하는 환경이다.

논리적인 개념이고, 눈에 보이지 않는다.

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

 

EntityManager.persist(entity)로 엔티티를 영속성 컨텍스트에 저장해 영속화 한다. 

 

 

 

엔티티의 생명 주기

 

-비영속 : 영속성 컨텍스트와 관계 없는 새로운 상태

객체만 생성되고 영속 컨텍스트에 저장되지 않은 상태

 

-영속: 객체 생성 후 엔티티 매니저에 persist를 해 영속성 컨텍스트에서 관리되는 상태

영속 상태에 있다고 DB에 쿼리에 날라가지 않는다. 트랜잭션을 커밋하는 순간 DB에 쿼리가 날라간다.

em.persist(member)

 

-준영속: 영속성 컨텍스트에 저장되었다가 분리된 상태

em.detatch(member)

 

-삭제: 삭제된 상태

em.remove(member)

 

 

 

1차 캐시

영속성 컨텍스트(EntityManager로 봐도 됨) 에는 1차 캐시가 있다.

PK로 키를, value로 엔티티 객체를 가진다.

 

em.persist(member)로 1차 캐시에 저장되고

em.find(Member.class, “member1”)로 조회를 하면 일단 1차 캐시를 뒤진다.

1차 캐시에 있으면 그 값을 그냥 조회한다.

만약에 1차 캐시에 없는 member2를 조회하려 하면, 1차 캐시를 조회해 없는것을 알고 DB를 조회해

객체를 1차 캐시에 저장한 후 반환한다.

이후에 member2를 다시 조회하려 하면 영속성 컨텍스트의 1차 캐시에 있는 member2가 반환되는 것이다.

 

엔티티 매니저는 데이터 트랜잭션 단위로 만들어지기 때문에, 고객의 요청이 들어와 비즈니스가 끝나면 영속성 컨텍스트를 지워버리고, 당연히 1차 캐시도 없어진다.

따라서 찰나의 순간만 사용을 하고 여러명이 사용하지 않는 캐시이다. 큰 성능은 없다.

 

Member member = new Member();
member.setId(101L);
member.setName("HelloJPA");

System.out.println("==BEFORE==");
em.persist(member);
System.out.println("==AFTER==");

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

 

1차 캐시에 있는걸 조회하기 때문에 db에 select문은 안나갔다.

 

Member member1 = em.find(Member.class, 101L); // db에서 가져와 영속성 컨텍스트에 올려놓는다.
Member member2 = em.find(Member.class, 101L); // 똑같은 걸로 조회하니까 쿼리가 안나가고 1차 캐시에서 가져온다. 

tx.commit();

 

위의 경우 조회가 한번만 나간다.

 

 

영속 엔티티 동일성 보장

Member member1 = em.find(Member.class, 101L); 
Member member2 = em.find(Member.class, 101L);

System.out.println("result = " + (member1 == member2)); // true

 

1차 캐시가 있기 때문에 같은 트랜잭션 안에서  == 비교시 true 이다.

 

 

쓰기 지연

memberA가 1차 캐시에 들어가고, 동시에 JPA가 엔티티를 분석해 insert 쿼리를 생성해 쓰기 지연 SQL 저장소에 넣는다.

memberB도 1차 캐시에 넣고, insert 쿼리를 쓰기 지연 SQL 저장소에 넣는다.

트랜잭션을 커밋하는 시점에 쓰기 지연 SQL 저장소에 있던 쿼리들이 flush가 되어 DB로 날라간다.

Member member1 = new Member(150L, "A");
Member member2 = new Member(160L, "B");

em.persist(member1);
em.persist(member2); // 여기 까지는 insert sql을 db 에 보내지 않는다.
System.out.println("============");

tx.commit(); // 커밋하는 순간 db에 insert sql을 보낸다.

 

버퍼링을 모아서 write 하는 것이다. 

 

<property name="hibernate.jdbc.batch_size" value="10"/>

 

hibernate.jdbc.batch_size 를 xml에 설정하면

지정한 사이즈만큼 모아서 데이터베이스에 한방에 쿼리들을 보내고 db를 커밋한다.

 

변경 감지(dirty checking)

 

커밋을 하면 내부적으로 flush가 호출된다.

 

*flush() : 영속성 컨텍스트의 변경 내용을 데이터베이스에 즉시 반영하는 작업

 

 

스냅샷은 영속성 컨텍스트의 1차캐시에 값을 읽어온 최초 시점의 상태를 찍어놓은 것이다.

 

객체의 값이 변경되면 JPA가 트랜잭션이 커밋되는 시점에 내부적으로 flush를 호출하며 엔티티와 스냅샷을 비교를 한다.

객체가 변경된걸 확인하고, update 쿼리를 쓰기 지연 SQL 저장소에 만들어둔다.

그리고 update 쿼리를 db에 반영해 커밋한다.

 

Member member = em.find(Member.class, 150L);
member.setName("zzzz");
System.out.println("==========");
tx.commit();

 

결과:

select 문 호출

======

update 문 호출

JPA는 값이 변경되면 트랜잭션이 커밋되는 시점에 변경을 반영한다.

 

플러시

 

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

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

 

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

 

플러시가 발생한다고 해서 데이터베이스 트랜잭션이 커밋되는 것은 아니다. 

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

  • em.flush()로 직접 호출
  • 트랜잭션 커밋되면 플러시 자동 호출
  • JPQL 쿼리를 실행해 플러시 자동 호출

 

*flush를 보낸다고 해서 1차 캐시를 지우진 않는다.

오직 영속성 컨텍스트의 쓰기 지연 SQL 저장소에 있는 쿼리들이 DB에 반영이 되는 것이다.

 

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

만약 em.persist()로 객체들을 저장한 후, 중간에 JPQL을 실행해 해당 객체들을 select하려 한다면

DB에 insert 하는 쿼리가 이전에 나간적 없으므로 select로 가져올 수 없다.

이런 경우를 방지하고자 기본 모드로 JPQL 쿼리를 실행할 때는 무조건 flush한다. 그 후에 JPQL의 쿼리가 날라간다.

 

플러시 모드 옵션

em.setFlushMode(FlushModeType.COMMIT)

  • FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러시(기본 값) - 권장
  • FlushModeType.COMMIT : 커밋할 때만 플러시 - 가급적이면 사용하지 말것
    • ex) persist 후에 persist한 객체 말고 다른 객체를 가져오는 JPQL 실행할 때는 굳이 쿼리를 실행할 때 플러시를 안해도 되므로 

 

 

준영속 상태

 

영속 상태의 엔티티가 영속성 컨텍스트에서 분리되는 것

dirty checking 등과 같은 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다. 

아래의 세가지 방법으로 엔티티를 준영속 상태로 만들 수 있다.

 

1. 특정 엔티티만 준영속 상태로 전환

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

em.detach(member); // 영속 상태에서 제거하기

System.out.println("=============");
tx.commit(); // 커밋할 때 아무 일도 일어나지 않는다.

 

결과:

select 문

======

 

2. 영속성 컨텍스트를 완전히 초기화

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

em.clear(); // 영속성 상태를 통째로 제거

Member member2 = em.find(Member.class, 150L); // 영속 상태에 아무것도 없으므로 쿼리를 날려 조회한다. 
System.out.println("=============");
tx.commit(); // 커밋할 때 아무 일도 일어나지 않는다.

 

결과:

select 문

select 문

======

 

3. 영속성 컨텍스트를 종료

 

em.close();

 

 

정리

💡 영속성 컨텍스트: 엔티티를 영구 저장하는 환경.

엔티티 매니저를 생성하면 그 안에 영속성 컨텍스트(=1차 캐시)가 있다.

영속 상태: em.persist()해서 넣거나 em.find()로 가져온 상태

준영속 상태: 영속성 컨텍스트에서 분리된 상태

 

 

 

 

 

인프런 자바 ORM 표준 JPA 프로그래밍 - 기본편을 수강하고 정리한 글입니다.

https://www.inflearn.com/course/ORM-JPA-Basic

'BACK > JPA' 카테고리의 다른 글

[JPA] JPA 프록시의 특징  (0) 2024.04.02
[JPA] TIL 6일차 - 엔티티 매핑  (1) 2024.04.02
[JPA] JPA 연관관계 매핑  (1) 2024.03.31
[JPA] TIL 4일차 - JPA 시작하기, 동작 방식  (1) 2024.03.31
[JPA] TIL 3일차 - JPA 소개  (0) 2024.03.30