본문 바로가기
카테고리 없음

[jpa] 영속성 컨텍스트의 이점 5가지

by dani0312 2024. 1. 13.

아래 글들에 걸쳐 영속성 컨텍스트에 대해 알아보았다. 또한 1차 캐시에 대해서도 함께 알아보았다. 

1차 캐시도 영속성 컨텍스트의 이점 중 하나였는데 이를 포함하여 이번 시간에는 영속성 컨텍스트의 이점 5가지에 대해 정리하고자 한다. 영속성 컨텍스트와 1차 캐시에 대한 개념이 필요하다면 아래 글을 먼저 참조하자

 

2023.12.23 - [웹 개발/jpa] - [jpa] JPA 영속성 컨텍스트란? with EntityManager #1

2024.01.11 - [웹 개발/jpa] - [jpa] 영속성 컨텍스트란 with 1차 캐시 #2

 

 

 

📌영속성 컨텍스트의 이점

 

영속성 컨텍스트의 이점

  1.  1차 캐시
  2. 동일성(identity) 보장
  3. 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
  4. 변경 감지(Dirty Checking)
  5. 지연 로딩(Lazy Loading)

 

◾1차 캐시

1차 캐시에 대해서는 아래 글에서 자세히 다루었기 때문에 아래 글을 먼저 참조하고 오는 것이 좋다.

 

2024.01.11 - [웹 개발/jpa] - [jpa] 영속성 컨텍스트란 with 1차 캐시 #2

 

[jpa] 영속성 컨텍스트란 with 1차 캐시 #2

본 글에 앞서 영속성 컨텍스트에 대해 잘 모른다면 아래 글을 먼저 참고하시는 것이 좋습니다. 2023.12.23 - [웹 개발/jpa] - [jpa] JPA 영속성 컨텍스트란? with EntityManager 이전 글에서는 영속성 컨텍스트

dani0312.tistory.com


본 글에서 다시 요약하자면, 1차 캐시는 영속성 컨텍스트 안에 존재하는 것으로 `em.persist()`를 통해 영속성 컨텍스트 안에 저장을 하게 될 때 이 1차캐시에 객체 정보가 저장이 된다.

 

객체를 `find()`를 통해 찾아오려 한다면 JPA는 데이터베이스가 아닌 이 1차캐시를 먼저 탐색하여, 여기에 객체가 있다면 반환하고 만일 없다면 데이터베이스를 조회한다. 객체가 1차 캐시에 있는 경우 데이터베이스까지 조회하지 않아도(쿼리를 날리지 않아도) 되는 이점이 존재한다. 

 

 

 

◾영속성 엔티티의 동일성 보장

 아래 코드를 살펴보자. a,b 둘 다 `member1`이라는 아이디의 객체를 조회해서 a,b에 반환하도록 하고 있다. 그렇다면 이와 같이 같은 객체를 각각 조회해오면, a와 b를 비교하였을 때 같다고 나올까? 

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");

System.out.println(a == b); //동일성 비교 true


결과는 `true`이다. JPA가 엔티티의 동일성을 보장해 주는 것이다. 이는 방금처럼 1차 캐시가 있기 때문에 가능한 것이다. 

 

주의할 점은 같은 트랜잭션 안에서만 1차 캐시가 유효하다고 하였다. 한 트랜잭션 안에서 엔티티매니저가 실행이 되고 종료가 되기 때문에 이 안의 영속성 컨텍스트, 그 안의 1차 캐시 또한 유효기간이 트랜잭션이 종료되기 전까지이다. 즉 위와 같이 `==` 동일성 비교를 하는 것도 같은 트랜잭션 안에서만 유효하다. 이것을 조금 어렵게 설명한다면 아래와 같다. 

 

1차 캐시로 반복 가능한 읽기 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공해준다. 이 설명은 조금 심화 설명이므로 이해가 가지 않는다면 'jpa에서 같은 트랜잭션 내에서는 엔티티의 동일성을 보장해 준다.'라고 이해하면 된다.

 

 

 

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

 엔티티를 등록할 때 트랜잭션을 지원하는 쓰기 지연이라는 것이 가능하다.

 

우리가 이전 1차 캐시에 대해서 이야기할 때 `em.persist()`하여도 데이터베이스에 바로 반영이 되는 것이 아니라고 하였다. 트랜잭션이 커밋될 때 반영이 된다고 하였다. 그렇다면 그 전에는 무슨 일이 일어나는 것일까?

 

영속성 컨텍스트 안에는 1차 캐시 말고도 `쓰기 지연 SQL저장소`라는 것이 존재한다. `em.persist()`를 하게 되면 JPA가 이 엔티티를 1차 캐시에 저장하고, 또 분석을 하여 INSERT 쿼리를 생성하여 쓰기 지연 SQL 저장소에 쌓아둔다. 아래 코드를 보자

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//👉여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

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

 

memberA와 memberB를 `em.persist()`를 통해 영속성 컨텍스트에 저장을 하고 있다. 그런데 조금 전, 1차 캐시 말고도 쓰기 지연 SQL저장소라는 것도 존재한다고 하였다. 그리하여 아래 그림처럼 persist() 를 호출할 때 INSERT 쿼리를 쓰기 지연 저장소에 저장을 하고, 1차 캐시에도 객체 정보를 저장하는 것이다. 

 

코드 흐름대로 memberA,B를 저장하는 것을 그림과 함께 살펴보자.

위의 그림은 memberA를 `em.persist()` 하였다. 그러면 영속성 컨텍스트 안에서 쓰기 지연 SQL 저장소에 쿼리가 저장이 되고, 1차 캐시에 객체 정보가 저장된다. (아직까지 db에는 아무런 반영도 하지 않았다)

 

이어서 memberB를 `em.persist()`한다. memberA와 같이, memberB에 대한 쿼리가 SQL저장소에 저장되고, 1차 캐시에 객체 정보가 저장된다. 현재 데이터베이스에는 반영이 되지 않았지만 쓰기 지연 SQL저장소에 memberA, memberB에 대한 INSERT 쿼리가 쌓여있는 상태이다. 

 

그리고나서 트랜잭션을 `commit()` 하게 되면 그때서야 쓰기 지연 SQL저장소에 묵혀있던 쿼리들이 DB에 반영이 된다. 트랜잭션 커밋 과정은 아래 [더보기]를 참조하자.

 

트랜잭션 커밋 과정

더보기

1. 트랜잭션.commit() 호출

2. flush()

쓰기 지연 SQL저장소의 쿼리들이 DB에 반영된다. 이것을 플러쉬라고 한다.

3. 데이터베이스 트랜잭션이 commit된다.

 

즉 정리하면, `em.persist()`를 통해 객체를 저장할 때 데이터베이스에 매번 '쓰기'를 하는 것이 아닌, 트랜잭션이 커밋되는 시점에 모아서 한 번에 쿼리를 보내고 데이터베이스를 커밋한다는 이점이 있다. 

 

 

 

◾ 변경 감지(엔티티 수정)

아래 코드를 살펴보면, 엔티티를 찾아와 그 엔티티의 이름을 수정하려 한다. 

try {
            Member member = em.find(Member.class,150L);
            member.setName("ZZZZZ");
            System.out.println("=======");
            tx.commit(); //결과: 선긋고 업데이트 쿼리가 나간다.

 

그런데, 수정하고 `em.persist()`를 호출하고 있지 않은데 콘솔 결과를 보니 UPDATE 쿼리가 발생했다. 어떻게 된 걸까? 

 

JPA는 이것을 Dirty Checking이라고 하는데, 변경 감지라는 기능으로 엔티티를 변경할 수 있는 기능이 제공된다.  이에 대한 해답은 영속성 콘텍스트 안에 있다. 아래 그림을 한 번 살펴보자.

 

1차 캐시에 Id, Entity가 저장이 된다고 하였는데 `스냅샷`이라는 것도 존재한다. 

💡스냅샷이란?
엔터티가 영속성 컨텍스트에 저장될 때, 해당 엔터티의 현재 상태를 복사하여 기록한 것이다. 주로 엔티티의 변경을 감지하고, 트랜잭션이 커밋되는 시점에 데이터베이스와의 변경을 확인하는데 사용된다.

객체를 setName()으로 수정을 하고 그 이후 아무런 코드(예를 들면 em.update())를 추가해주지 않았는데 업데이트 쿼리가 생성되는 것의 비밀은 이 스냅샷에 있다. JPA는 데이터베이스를 커밋하면 아래와 같은 일이 벌어진다.

 

 

트랜잭션.commit() 호출 시 일어나는 일

1. flush()

2. 엔티티와 스냅샷을 비교한다.

3. 변경사항이 있다면 업데이트 쿼리를 저장소에 쌓아둔다.

4. 저장소의 쿼리들이 데이터베이스에 반영된다. 

 

트랜잭션을 `commit()`하게 되면 우선 flush를 호출한다. 그리고 엔티티와 스냅샷을 비교한다. 그런데 우리가 멤버의 값을 변경하였다. 그러면 스냅샷과 다를 것이다. JPA가 이를 보고 변경사항이 있으면 업데이트 쿼리를 쓰기 지연 SQL저장소에 만들어둔다. 이것을 데이터베이스에 반영을 하며 변경을 하게 되는 것이다. 이것을 변경 감지(Dirty Checking)라고 한다.

 

 

 

◾지연 로딩

 연관된 엔티티가 실제로 사용될 때까지 로딩을 지연시킬 수 있다. 이 역시 트랜잭션 범위 내에서 지연 로딩이 되는 것이다. 이는 성능 최적화에 도움이 되며, 모든 연관된 엔티티를 즉시 로딩하지 않아도 된다.

`fetch`관련 설정을 통해 데이터를 필요할 때 로딩을 하게 해 두는데, 실무에서도 많이 사용되는 방법이다.

 

 

 

 

 

 


참고

자바 ORM 표준 JPA 프로그래밍 - 기본편 / 김영한 / 인프런 강의


/* 내가 추가한 코드 */ /* 내가 추가한 코드 끝끝 */