[JPA] 영속성 컨텍스트란?

김운채·2023년 9월 1일
0

Spring

목록 보기
7/10

1. 영속성 컨텍스트

엔티티를 영구 저장하는 환경이라는 뜻으로, 애플리케이션이 데이터베이스에서 꺼내온 객체를 보관하는 역할을 한다. 영속성 컨텍스트는 엔티티 매니저(Entity Manager)를 통해 엔티티를 조회하거나 저장할때 엔티티를 보관하고 관리한다.

1-1. Entity Manager ?

Entity Manager 이놈은 엔티티를 만들어서 관리하는 클래스이다.

엔티티
엔티티(entity)는 데이터베이스의 테이블과 매핑되는 객체를 의미. 본질적으로는 우리가 흔히 부르는 일반 객체인데, 데이터베이스의 테이블과 직접 연결된다는 아주 특별한 특징이 있어 구분지어 부른다고 함. 즉, 엔티티는 객체이긴 하지만 데이터베이스에 영향을 미치는 쿼리를 실행하는 객체인 것.

Entity Manager 는 EntityManagerFactory 가 만들어 관리한다.

그럼 엔티티 어쩌고 하는 애들은 DB랑 관련이 있다고 했다. 데이터베이스엔 여러 사용자가 접근이 가능하다.

EntityManagerFactory는 여러 스레드에서 동시에 접근해도 안전하지만, 생성하는 비용이 상당히 크다. 그래서 데이터베이스당 1개만 만들어서 애플리케이션 전체에 공유하여 쓰도록 설계되어 있다.

요청이 올때마다 공장을 지을 순 없으니, EntityManagerFactory에서 비용이 적은 EntityManager를 생성한다. (EntityManager는 Thread Safe하지 않아, 여러 스레드가 동시에 접근하면 동시성 문제가 발생한다 = 요청(스레드)별로 한 개 씩 할당)

예를 들어, 회원 2명이 동시에 회원가입을 하려고 할때(고객 요청) -> 회원 1의 요청에 대해서 가입 처리를 할 Entity Manager를 EntityManagerFactory가 생성하면(1스레드 1 EntityManager) -> 여길 통해서 가입 처리후 데이터베이스에 회원 정보를 저장하는 것이다.(Transaction이 종료되면 해당 스레드를 종료)

회원 2도 마찬가지다. 그리고 회원 1, 2를 위해 생성된 Entity Manager는 필요한 시점에 데이터베이스와 연결한 뒤에 쿼리를 날린다. (EntityManager는 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다.)

1-2. 스프링부트의 EntityManagerFactory

스프링 부트에서도 직접 엔티티 매니저 팩토리를 만들어서 관리할까?
답은 no 라능.. 🙅‍♂️

스프링 부트는 내부에서 엔티티 매니저 팩토리를 하나만 생성해서 관리하고 @PersistenceContext 또는 @Autowired 애너테이션을 사용해서 엔티티 매니저를 사용한다.

@PersistenceContext
EntityManager em; // 프록시 엔티티 매니저. 필요할 때 진짜 엔티티 매니저 호출

그리고 스프링 부트는 기본적으로 빈은 하나만 생성해서 공유하므로 동시성 문제가 발생할 수 있다. 그래서 가짜 분신을 만들어서(프록시) 사용한다. 필요할때 데이터베이스 트랜잭션과 관련된 실제 엔티티 매니저를 호출하는 거라고 함..

📌 쉽게 말해 엔티티 매니저는 Spring Data JPA에서 관리하므로 내가 직접 생성하거나 관리할 필요가 없다!


2. 영속성 컨텍스트의 특징

영속성 컨텍스트에는 몇가지 중요한 특징이 있는데, 간단하게 알아보자.

2-1. 1차 캐시

1차 캐시.. 단어만 들어도 뭔가 저장해서 쓰나? 하는 삘이 온다.

영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데, 이것을 1차 캐시라 한다. 영속 상태의 엔티티는 모두 이곳에 저장되는 것이다. 1차 캐시는 트랜잭션을 시작하고 종료할 때까지만 유효하다.

영속성 컨텍스트 내부에 Map이 하나 있는데 (1차 캐시), 키는 @Id로 매핑한 식별자고 값은 엔티티 인스턴스다.

  • 1차 캐시의 흐름
  1. 최초 조회할 때는 1차 캐시에 엔티티가 없다.
  2. save 시 엔티티를 1차 캐시와 db에 보관한다.
  3. 1차 캐시에 보관된 결과를 반환한다.
  4. 이후 같은 find를 했을 때 해당 엔티티가 1차 캐시에 존재하면 1차 캐시에 저장된 엔티티를 반환하고 존재하지 않으면 DB를 조회한다.
    👉 이를 통해 캐시된 데이터를 조회할 때에는 데이터베이스를 거치치 않아도 되니, 매우 빠르게 데이터를 조회할 수 있다.

2-2. 동일성 보장

1차 캐시는 객체의 동일성 (a == b)를 보장한다.

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

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

em.find(Member.class, "member1"); 을 반복해서 호출해도 영속성 컨텍스트는 1차 캐시에 있는 같은 인스턴스를 반환한다.
이러한 이유로 영속 엔티티의 동일성(== 비교)를 보장할 수 있다.


2-3. 쓰기 지연(transactional write-behind)

쓰기 지연? 하.. 뭔말일까 감도 안온다 🤷‍♀️

근데 사실 간단하다.

쓰기 지연은 트랜잭션을 커밋하기 전까지는 데이터베이스에 쿼리문을 보내지 않고 쿼리를 모았다가~ 트랜잭션을 커밋하면 모았던 쿼리를 한번에 실행하는 것을 의미한다.

트랜잭션 내부에서 persist()가 일어날 때, 엔티티들을 1차 캐시에 저장하고, 쓰기 지연 SQL 저장소에 INSERT 쿼리들을 생성해서 쌓아 놓는다. DB에 바로 넣지 않고 기다린다.
commit() 또는 flush() 를 할때 쓰기지연 SQL 저장소에 저장되어 있는 SQL들을 DB에 보낸다.

👉 이를 통해 데이터베이스 시스템의 부담을 줄일 수 있다.

🤷‍♀️ 근데... 쿼리 한방에 보내는게 시스템의 어떤 부담을 줄일 수 있다는 겨?

쓰기지연저장소를 사용함으로써 DB커넥션 시간을 줄일 수 있고, 한 트랜잭션이 테이블에 접근하는 시간을 줄일 수 있다.
쓰기지연저장소가 없다면 쌓아둠 없이 매번 쿼리를 날려야할테니 디비에 연결 ~ 해제 하는 과정이 매번필요하고 매번 테이블에 접근해야하는데 이때 시간이 걸린다. 그래서 쓰기 지연 저장소를 활용하면 이 시간을 줄일 수 있게 된다


2-4. 변경 감지

엔터티의 값을 변경하면 영속성 컨텍스트 내의 쿼리 저장소에 쌓아두다가, 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영한다.
즉, Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없게 되는 것이다. 이 개념을 변경감지, 더티 체킹(dirty checking)이라고 한다.

JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장하는데 이를 스냅샷이라 한다.
플러시 시점에 스냅샷과 엔티티를 비교해서 엔티티를 찾고, 만일 스냅샷과 비교하여 변경된 내용이 있을 경우 Update Query를 날린다.

그럼 변경 감지로 인해 실행된 UPDATE SQL의 쿼리는 변경된 부분만 수정 쿼리가 생성될까?
ㄴㄴ
모든 필드를 업데이트 하는게 JPA의 기본 전략이다.
JPA는 애플리케이션 로딩 시점에 update쿼리를 미리 만들어두고 계속 재사용하면서 써먹는다.
이렇게 한번 파싱된 쿼리를 재사용함으로써 성능을 높인다.

🤷‍♀️ 성능이 어떤 방식으로 높아질까?
여기서 Statement와 PreparedStatement의 차이점을 알 수 있다. 참고글 👉Statement와-PreparedStatement

이 글에서 sql을 string 객체로 저장한다고 했다. 우리 모두가 알다시피 string 객체는 저장되는 방식이 특수하다. 참고글 👉 String equals() 와 == 의 차이, String Pool

DB는 1차적으로 쿼리를 캐시한다. 자기가 뭘 해야할지 알수있게 한번 해석하는 과정을 거쳐야한다.
이렇게 저당되는 string 객체의 내용이 한글자만 달라져도 매번 캐시하고 해석하는 과정을 거쳐야하는 것이다.

하지만 JPA는 PreparedStatement 로 쿼리를 날리기 때문에 파라미터만 따로 주면, 미리 만들어둔 update쿼리를 재사용해서 전체로 날리는 것이다.

아니 근데 만약 수정한 컬럼만 수정하고 싶어!!! 🤸‍♂️ 한다면 @DynamicUpdate 를 사용하면 된다.

@Entity
@Table(name = "table")
@DynamicUpdate
public class Entity {}

0개의 댓글