본문 바로가기
Spring boot/JPA

JPA 영속성 컨텍스트

by eunnnn 2023. 4. 17.

JPA 엔티티

JPA에서 말하는 엔티티란 테이블에 대응하는 하나의 클래스라고 생각하면 된다. 

@Entity
public class Account {
  String username;
  String password;
}

그래서 JPA에서 '하나의 엔티티 타입을 생성한다' 라는 말의 의미는, '하나의 클래스를 작성한다'는 의미와 동일하다.

 

 

엔티티 매니저

엔티티 매니저란 여러 엔티티 객체를 관리하는 역할을 하는 것을 말한다. 여기서 말하는 '관리'란 엔티티 객체의 Life Cycle을 관리하는 것이다. 즉 엔티티 매니저는 관리하는 엔티티 객체들을 영속 컨텍스트(Persistence Context)에 넣어두고, 객체들의 생사를 관리한다.

 

이러한 엔티티 매니저는 엔티티 매니저 팩토리를 통해서 생성된다. 엔티티 매니저 팩토리가 생성되는 시점에 DB 커넥션 풀을 생성해 둔 후, 고객의 요청이 들어올 때 마다 엔티티 매니저를 생성한다. 그리고 엔티티 매니저는 DB 연결이 필요한 시점에 커넥션 풀에 있는 Connection을 얻는다)  

 

영속성 컨텍스트

영속성 컨텍스트란 엔티티를 영구히 저장하는 환경을 말한다. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관, 관리한다.  

 

영속성 컨텍스트의 특징은 다음과 같다. 

  • @Id 필드를 이용하여 엔티티를 식별한다. 따라서 엔티티를 정의할 때 식별자 값이 꼭 있어야 한다.
  • 쓰기 지연 기능이 존재한다 -> 값을 변경하자마자 바로 DB에 반영하는 것이 아니라, persistence context에 쓰기 지연 SQL 저장소가 있어서 SQL 쿼리들을 저장해 뒀다가, 엔티티 매니저가 commit() 메서드를 호출할 때 DB에 반영된다

예시를 들어보자면, 어떤 객체(엔티티)의 값을 저장하기 위해서 persist() 메서드를 쓴다면 이 경우 객체가 바로 DB로 INSERT 되는 것이 아니라 영속성 컨텍스트에 등록되는 것이다.

 

위위 도식에서처럼 JPA를 이용하여 어떤 객체를 조회하거나 persist 하는 경우, DB에 바로 접근하는 것이 아니라 중간 매개체인 영속성 컨텍스트에 접근하게 된다. 그중 1차 캐시에 접근하여 해당 객체에 대한 정보가 있는지를 검색하는데, 만약 1차 캐시에 객체 정보가 없다면 DB에 쿼리를 날려서 값을 가져오게 된다. 그리고는 1차 캐시에 저장하고 요청한 값을 반환한다.

 

이런 과정에 따라 엔티티의 상태를 구분하는데, 이를 엔티티 생명주기라고 한다. 

 

엔티티 생명주기

 

엔티티에는 4가지의 상태가 존재한다

  • 비영속 (new) : 엔티티가 영속성 컨텍스트에 의해 관리되지 않는 상태. 처음 객체를 생성하고 persist 하기 전의 객체는 영속성 컨텍스트에 올라가지 않은 상태이므로 비영속 상태다.
  • 영속 (managed) : 1차 캐시에 엔티티가 등록되어 관리되는 상태. 이 상태에서 같은 객체를 다시 한번 조회하게 되면, 굳이 1차 캐시상에서 값을 바로 가져온다
  • 준영속 (detached) : 영속성 컨텍스트에서 해당 객체가 떨어져 나온 경우를 의미. 이렇게 되면 객체의 필드값을 변경하더라도 DB에 반영되지 않고 변경 감지(dirty checking)이 되지 않는다.
  • 삭제 (removed) : 속성 컨텍스트 뿐아니라 실제 DB에서 해당 객체에 대한 정보를 삭제한다는 의미가 된다.

 

영속성 컨텍스트의 특징 - flush

영속성 컨텍스트의 변경 내용을 DB 에 반영하는 것을 말한다.
Transaction commit 이 일어날 때 flush가 동작하는데, 이때 쓰기 지연 저장소에 쌓아 놨던 INSERT, UPDATE, DELETE SQL들이 DB에 날아간다.

  1. 변경을 감지한다. (Dirty Checking)
  2. 수정된 Entity를 쓰기 지연 SQL 저장소에 등록한다.
  3. 쓰기 지연 SQL 저장소의 Query를 DB에 전송한다. (등록, 수정, 삭제 Query)

영속성 컨텍스트를 flush 하기 위해서는, 아래와 같은 방법이 존재했다.

  1. em.flush()를 통한 직접호출
  2. 트랜잭션 커밋 시 플러시 자동 호출
  3.  JPQL 쿼리실행 시 플러시 자동 호출

 

영속성 컨텍스트가 엔티티를 관리할 때 얻는 장점 

1. 1차 캐시

영속성 컨텍스트 내부에는 캐시가 있는데 이를 1차 캐시라고 한다. 영속 상태의 엔티티를 이곳에 저장한다. 1차 캐시의 키는 식별자 값(데이터베이스의 기본 키)이고 값은 엔티티 인스턴스이다. 

 

영속성 컨텍스트 내부에서 "member1" 엔티티를 조회하려면 다음과 같은 과정을 거친다.

1. "member1"조회 요청 시 JPA는 영속성 컨텍스트의 1차 캐시에서 해당 PK값을 가진 Member객체를 찾는다.

2. 1차 캐시에 존재한다면 그 값을 반환하고, 존재하지 않는다면 DB를 조회하여 값을 가져온다. (select 쿼리 자체가 나가지 않는다)

3. DB를 조회하여 값을 가져온 경우라면, 가져온 값을 1차 캐시에 저장해둔다.

4. 다음번에 "member1"을 조회하면, DB를 거치지 않고 1차 캐시에서 가져올 수 있게 된다. 

원래는 조회를 하기 위해서는 DB에 연결해서, DB에 존재하는 Member 테이블의 값들 중 PK가 member1 인 값을 찾아오는 SQL을 수행해야 한다. 그러나 1차 캐시 덕분에 DB에 SQL을 수행하지 않아도 member의 값을 가져올 수 있고, 이는 성능상 이점을 갖는다.

 

2. 동일성 보장

JPA는 하나의 트랜잭션 안에 존재하는(= 1차 캐시 내에 존재하는) Id(식별자 값)가 같은 엔티티에 대해서 동일성을 보장해준다.

//Id가 1인 Member 엔티티 조회
Member findMember1 = em.find(Member.class, 1L);
Member findMember2 = em.find(Member.class, 1L);

System.out.println(findMember1 == findMember2);//true가 반환

첫 번째 find를 통해 DB에서 Member를 조회한다. 그리고 조회한 멤버를 1차캐시에 등록한다.

두 번째 find는 1차 캐시에서 값을 확인했을 때 존재하기 때문에, 1차 캐시에 존재하는 값을 가져옵니다.

이 때 1차 캐시에서 참조하고 있는 객체는 처음 조회한 객체와 같은 객체이므로 동일성 비교 결과 true가 반환된다.

 

3. 쓰기 지연

엔티티를 조회, 저장, 수정 등의 작업이 일어나면 바로 SQL문이 DB에 적용되는 것이 아니라 쓰기 지연 SQL 저장소라는 곳에 SQL문이 쌓인다. JPA는 이렇게 생성된 SQL문들을 플러시(flush)가 일어나는 경우 DB에 쿼리를 보내게 된다. 

4. 변경 감지

영속 상태로 관리되는, em.find나 em.persist 처리된 객체는 1차 캐시에 등록된다. 그리고 등록될 당시의 필드값 등의 정보가 위 도식에서 Snapshot이라는 상태로 저장된다. JPA는 어떤 객체의 필드값이 변경되면 이 Snapshot일때의 상태와 비교하여 1차 캐시와 DB에 해당 객체의 정보를 업데이트 해준다.

 

Member memberA = em.find(Member.class, 10L);

memberA.setUsername("modified name!");

 

위 코드와 같이 영속 상태의 객체의 필드값을 set 해주는 것만으로도 따로 update나 save가 필요없다. 나중에 쿼리가 나갈 때 자동으로 변경사항이 반영되어 DB에 update 된다.

 

5. 지연 로딩

엔티티에서 해당 엔티티를 불러올 때 그 때 SQL을 날려 해당 데이터를 가져온다.

 

더보기

'Spring boot > JPA' 카테고리의 다른 글

JPA N+1 문제  (0) 2023.04.17
ORM과 JPA  (0) 2023.02.03