본문 바로가기
BackEnd/JPA

[JPA] 영속성 관리 - 내부 동작 방식

by 개발 Blog 2024. 7. 12.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

영속성 컨텍스트1

JPA에서 가장 중요한 2가지

1. 객체와 관계형 데이터베이스 매핑하기

2. 영속성 컨텍스트

 

- 고객 요청이 올 때마다 엔티티 매니터 팩토리를 통해서 앤티티 매니저를 생성한다.

- 앤티티 매니저는 내부적으로 DB 커넥션을 사용해서 DB에 접근한다.

 

그러면 영속성 컨텍스트란 뭘까?

 

영속성 컨텍스트

- JPA를 이해하는데 가장 중요한 용어- 엔티티를 영구 저장하는 환경이라는 뜻

- EntityManger.persist(entity); 

ㄴ persist 메서드는 DB에 저장하는 것이 아니라, 엔티티를 영속성 컨텍스트에 저장한다.

- 논리적 개념으로 눈에 보이지 않는다.

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

 

- 앤티티 매니저를 생성하면 그 안에 1:1로 영속성 컨텍스트가 생성이 된다.

 

엔티티의 생명주기

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

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

- 영속 (managed)
: 영속성 컨텍스트에 관리되는 상태

//객체를 생성한 상태(비영속)
Member member= new Member();
member.setId(100L);
member.setName("HelloJPA");

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

//객체를 저장한 상태(영속)
em.persist(member);

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

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

- 삭제 (removed)
: 삭제된 상태

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


영속성 컨텍스트의 이점

- 1차 캐시 동일성(identity) 보장

- 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)

- 변경 감지(Dirty Checking) 지연 로딩(Lazy Loading)

 

영속성 컨텍스트2

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

- 영속성 컨텍스트는 내부에 1차 캐시가 있다.

- 멤버 객체를 생성하고 값을 세팅한다. (비영속 상태)

- em.persist(member); 로 만든 멤버를 파라미터로 받으면 1차 캐시에 Key와 Value로 저장된다.

 

이렇게 저장하면 조회할 때 이점이 있다.

 

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

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

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

- em.find로 조회를 하면 DB에서 찾는 게 아니고 먼저 1차 캐시에서 찾는다.

- 그런데 1차 캐시에 없는 "member2"를 조회하면 DB에서 값을 찾아서 1차 캐시에 저장 후 반환한다.

- 그리고 다음에 "member2"를 조회하면 1차 캐시에 있기 때문에 쿼리가 나가지 않는다.

 

영속 엔티티의 동일성 보장

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); //동일성 비교 true

같은 트랜잭션 안에서 비교했을 때

- JPA는 == 비교의 동일성을 보장해 준다. 마치 자바 컬렉션에서 똑같은 레퍼런스가 있는 객체를 꺼내면 똑같은 것처럼 말이다.

- 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공

- 1차 캐시가 있기 때문에 가능하다.

 

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

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), em.persist(memberB)를 작성했을 때 1차 캐시에 저장되고, DB에 보내지 않는다.

- 그리고 동시에 JPA가 엔티티를(memberA, memberB) 분석해서 INSERT 쿼리를 생성 후 쓰기 지연 SQL 저장소에 쌓아둔다.

 

- 트랜잭션을 commit 하는 순간 쓰기 지연 SQL 저장소에 있던 INSERT 쿼리들이 플러쉬가 되면서 DB로 간다.

 

그냥 바로바로 쿼리 날려서 DB에 보내면 안 될까?? 하는 의문이 생긴다. 

 

쿼리를 매번 DB에 날리면 성능 최적화를 할 수 없다. 하지만 버퍼링 같은 기능을 사용해 쿼리를 모아 한 번에 DB에 보내면 여러 번 쿼리를 날리는 것을 피할 수 있어 성능 최적화를 할 수 있다.

 

 

엔티티 수정 - 변경 감지

EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();

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

    //em.update(member) 이런 코드가 있어야 하지 않을까?

    System.out.println("==================");
    tx.commit();
}

- ID가 150인 데이터를 찾고 A -> ZZZZZ로 변경됐다. update 쿼리를 쓴 적이 없지만 update 쿼리가 나간 것을 볼 수 있다.

- 이는 JPA의 변경감지 기능 때문이다.

 

- JPA는 최초로 1차 캐시에 들어온 값들을 스냅샷을 떠둔다.

- JPA는 트랜잭션을 커밋하는 시점에 내부적으로 flush()를 호출한다.

- flush()가 호출되면 JPA가 엔티티랑 스냅샷을 비교해서 변경된 값이 있으면 update 쿼리를 쓰기 지연 SQL 저장소에 만들어 둔다.

- 그리고 flush 되면서 DB에 반영을 한다. 이를 변경감지라고 한다.

 

엔티티 삭제

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

- 삭제도 위에 설명한 것과 같은 방식으로 동작한다.

 

따라서 JPA는 값을 바꾸면 트랜잭션이 커밋되는 시점에 update 한다고 생각하면 된다.

 

플러시

플러시는 영속성 컨텍스트의 변경내용을 데이터베이스에 반영한다.

 

플러시 발생

- 변경사항을 감지한다.

- 수정된 엔티티에 대한 SQL을 쓰기 지연 SQL 저장소에 등록한다.

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

 

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

-  em.flush() : 직접 호출

- transaction commit : 플러시 자동 호출

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

 

 데이터를 commit 전에 DB에 보내고 싶으면 flush()를 호출하면 된다.

 //영속
Member member = new Member(200L, "member200");
em.persist(member);

em.flush();

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

- commit 전에 DB에 반영되는 것을 볼 수 있다.

 

그리고 flush()를 하면 1차 캐시에 데이터가 지워지는 것이 아니고, 쓰기 지연 SQL 저장소에 있는 쿼리들만 반영된다.

 

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);

//중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members= query.getResultList();

- 위 코드처럼 commit 전에 쿼리를 작성하면 DB에서 조회가 되지 않는다. 

- JPA는 이런 현상을 방지하고자 JPAL을 실행하면 무조건 flush()가 호출되도록 한다.

 

flush() 정리 

- 영속성 컨텍스트를 비우지 않는다.

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

- 트랜잭션이라는 작업 단위가 중요하다.

- 커밋 직전에만 동기화하면 된다.

 

준영속 상태

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

그렇게 되면 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.

 

준영속 상태로 만드는 방법

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

em.detach(member); //member 엩티티를 영속성 컨텍스트에서 제거 

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

- update 쿼리가 나가지 않았다. 

- detach()를 사용하면 특정 엔티티만 준영속 상태로 전환한다.

 

Member member = em.find(Member.class, 150L);
member.setName("ZZZZZ");

em.clear();

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

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

- em.clear()는 영속성 컨텍스트 전체를 완전히 초기화한다.

- 1차 캐시에 값이 없기 때문에 select 쿼리가 두 번 날아간다. (다시 영속성 컨텍스트에 올리는 작업)

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

 

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

[JPA] 고급 매핑  (0) 2024.07.15
[JPA] 다양한 연관관계 매핑  (0) 2024.07.14
[JPA] 연관관계 매핑 기초  (2) 2024.07.14
[JPA] 엔티티 매핑  (0) 2024.07.12
[JPA] JPA 시작하기  (0) 2024.07.11