[JPA] JPA 영속성 컨텍스트(+ flush, 준영속 상태)
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.
2023.02.09 - [0 + 프로그래밍/0 + JPA] - JPA 구동 방식과 간단 실습(CRUD)
JPA
를 학습하면 반드시 들어보신 말이 바로 영속성 컨텍스트 입니다.
그만큼 중요하기 때문인데요.
영속성 컨텍스트가 무엇인지 한 번 제대로 알아봅시다.
영속성 컨텍스트
영속성 컨텍스트는 엔티티를 영구 저장하는 환경이라는 의미 합니다.EntityManager.persist(entity);
를 사용하면 INSERT
쿼리가 발생했다고 생각할 수 있습니다.
persist(entity)를 했다고 INSERT SQL이 DB에 발생한것이 아닙니다. 아닙니다. 아닙니다.
persist(entity)
는 entity
를 영속성 컨텍스트에 저장한다는 의미 입니다.
영속성 컨텍스트는 논리적인 개념입니다.(🙈 눈에 보이지 않아요..ㅎㅎ)JPA
는 EntityManager
를 통해서 영속성 컨텍스트에 접근 합니다.EntityManager
를 생성하면 그안에 PersistenceContext
가 생성 됩니다.
영속성 컨텍스트를 이해하기 위해서는 먼저 엔티티의 생명주기에 대해서 알아야 합니다.
엔티티 생명주기(Entity Life Cycle)
비영속(new/transient)
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태 입니다.
// 객체를 생성한 상태(비영속)
Member member = new Member();
member.setId(1L);
member.setUsername("이용진");
영속(menaged)
영속성 컨텍스트에 관리되는 상태 입니다.
// 객체를 생성한 상태(비영속)
Member member = new Member();
member.setId(1L);
member.setUsername("이용진");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//객체를 저장한 상태(영속)
em.persist(member); // INSERT 쿼리 생성 X
em.persist(member);
에서 진짜로 INSERT
쿼리가 발생하지 않는지 확인해 봅시다.
실험
public class JpaMainV1 {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
// 비영속
Member member = new Member();
member.setId(1L);
member.setName("이용진");
System.out.println("persist(member) 전");
em.persist(member); // 영속
System.out.println("persist(member) 후");
tx.commit(); // 커밋
}
}
결과
persist(member) 전
persist(member) 후
Hibernate:
/* insert com.study.purejpa.entity.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
😯 오잉?! 진짜로 persist(member) 전
, persist(member) 후
사이에 쿼리가 생기지 않았습니다.!!!
결론
결론부터 말씀드리면,
영속성 컨텍스트에 저장 됐다고 해서 DB에 쿼리가 날라가는게 아닙니다.
그러면 언제 쿼리가 날라가는거지?(쓰기 지연 SQL 저장소에 저장되어 있음ㅎㅎ)
DB에는 커밋 이후에 저장됩니다…!!!!!!!!!!!!!!!!!!!!!!!!
준영속(detached)
영속성 컨텍스트에 저장되었다가 분리된 상태 입니다.
// 회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
삭제(removed)
삭제된 상태 입니다.
// 객체를 삭제한 상태(삭제)
em.remove(member);
🤔 그렇다면 왜 JPA
는 영속성 컨텍스트로 엔티티를 관리하는 걸까요?
영속성 컨텍스트 장점
- 1차 캐시
- 동일성(
identity
) 보장 - 트랜잭션을 지원하는 쓰기 지연(
transactional write-behind
) - 변경 감지(
Dirty Checking
) - 지연 로딩(
Lazy Loading
)
1차 캐시
// 객체를 생성한 상태(비영속)
Member member = new Member();
member.setId(1L);
member.setUsername("이용진");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//객체를 저장한 상태(영속)
em.persist(member); // INSERT 쿼리 생성 X
// 1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
member
를 영속성 상태로 만들면 다음과 같은 현상이 발생합니다.
그리고 조회를 하면 다음과 같은 현상이 발생합니다.
만약 1차 캐시에 엔티티가 존재하지 않으면 DB에서 조회하게 됩니다.
해당 과정을 직접 코드로 확인해 볼까요?
실험
public class JpaMainV1 {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
// 비영속
Member member = new Member();
member.setId(1L);
member.setName("이용진");
System.out.println("persist(member) 전");
em.persist(member); // 영속
System.out.println("persist(member) 후");
// member가 영속성 컨텍스트로 관리 되기 때문에 SELECT 쿼리 생성 X
Member findMember = em.find(Member.class, 1L);
System.out.println("memberName="+findMember.getName());
tx.commit(); // 커밋
}
}
결과
persist(member) 전
persist(member) 후
memberName=이용진
Hibernate:
/* insert com.study.purejpa.entity.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
1차 캐시에 member
가 존재하기 때문에 SELECT
가 생성되지 않는것을 확인 할수 있습니다.
참고
1차 캐시는 1개의 트랜잭션 안에서만 이루어지는 과정이기 때문에 엄청나게 큰 이점은 없습니다...ㅎㅎ
동일성(identity) 보장
1차 캐시로 반복 가능한 읽기(REPEATABLE READ
)등급의 트랜잭션 격리 수준을DB
가 아닌 애플리케이션 차원에서 제공합니다.(1차 캐시가 있기 때문에 가능!)
(필자는 동일성 보장의 효과가 어떤 것인지 파악을 하지 못했습니다....)
public class JpaMainV1 {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
// 비영속
Member member = new Member();
member.setId(1L);
member.setName("이용진");
em.persist(member); // 영속
// member가 영속성 컨텍스트로 관리 되기 때문에 SELECT 쿼리 생성 X
Member findMember1 = em.find(Member.class, 1L);
Member findMember2 = em.find(Member.class, 1L);
// 동일성 비교 true
System.out.println("is true? "+(findMember1 == findMember2));
tx.commit(); // 커밋
}
}
결과
동일성 보장은 1차 캐쉬가 있기 때문에 가능합니다.
is true? true
트랜잭션을 지원하는 쓰기 지연(transaction write-behind)
transaction write-behind
는 persist(entity)
일때 SQL
을 데이터 베이스에 보내지 않는 것을 의미합니다.
(쓰기 지연 SQL 저장소에 저장 ㅎㅎ)
앞에서 영속 상태를 학습할 때 확인해본 내용 입니다.
(커밋하는 순간 데이터베이스에 INSERT SQL
을 보낸다.!!)
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(member1);
em.persist(member2);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않음.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보냄.
transaction.commit(); // [트랜잭션] 커밋
쓰기 지연 동작 방식은 다음과 같습니다.member1
영속
member2
영속
쓰기 지연 SQL
저장소에 SQL
이 쌓이는 것을 확인 할수 있습니다.
commit()
을 하게되면 다음과 같이 동작합니다.
flush
가 발생한뒤 DB에서 commit
이 발생하게 됩니다.
실험
이러한 과정을 코드로 작성해보겠습니다.^^
public class JpaMainV1 {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
// 비영속
Member member1 = new Member(1L, "유재석");
Member member2 = new Member(2L, "박명수");
// 영속성 컨텐스트에 저장, 쓰기 지연 SQL 저장소에 SQL 저장
em.persist(member1);
em.persist(member2);
System.out.println("persist 이후에 쿼리 나감!");
tx.commit(); // 커밋(쿼리 발생)
}
}
결과
persist 이후에 쿼리 나감!
Hibernate:
/* insert com.study.purejpa.entity.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
Hibernate:
/* insert com.study.purejpa.entity.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
JPA
는 이렇게 쓰기 지연을 하는 이유가 뭘까? 🤔
만약 persist(entity)
를 하는 동시에 쿼리를 DB
에 날린다면, 개발자가 최적화 할수 있는 상황이 사라지게 됩니다.
예를 들어 현재 em.persist(member1); em.persist(member2);
처럼 쓰기 지연 SQL 저장소에 쿼리가 쌓이게 되면 이 쿼리를 한번에 DB에 보낼수 있습니다.
이것을 우리는 JDBC Batch
라고 합니다.
버퍼링 같은 기능이라고 생각하면 쉽습니다. (말 그대로 모았다가 한방에 보냅니다.)
hibernate
의 옵션을 설정해주면 이와같은 기능을 사용할 수 있습니다.<property name=“hibernate.jdbc.batch_size” value=“10”/>
식으로 설정 하면 됩니다.
변경 감지(dirty checking)
바로 코드로 보여드리겠습니다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // [트랜잭션] 시작
// 영속 엔티티 조회
Member member1 = em.find(Member.class, 1L);
// 영속 엔티티 데이터 수정(변경 감지!)
member1.setUsername("유재석");
//em.update(member) 이런 코드가 있어야 하지 않을까?
transaction.commit(); // [트랜잭션] 커밋
변경 감지 동작 방식은 다음과 같습니다.
스냅샷이라는 것을 통해 변경 감지를 하게되는데,
여기에서 스냅샷이란 DB를 통해 엔티티를 처음 읽어오거나 영속화한 시점의(객체가 특정 시점에 구체적으로 어떤 상태 인지) 엔티티를 저장하는 것 입니다.
JPA
는 엔티티의 상태가 변경되면 스냅샷을 통해 저장해둔 엔티티와 비교를 하게 됩니다.
이때 스냅샷의 상태와 다른 엔티티가 있다면,
해당 엔티티의 상태를 변경해 버리는 UPDATE
쿼리를 쓰기 지연 SQL
저장소에 쌓게 됩니다.
그 이후에 DB
에 쿼리를 날리게 되는 것(flush
) 입니다.
실험
이러한 과정을 코드를 통해 확인해 봅시다.
public class JpaMainV1 {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin(); // 트랜잭션 시작
// 비영속
Member member1 = new Member(1L, "유재석");
// 영속성 컨텐스트에 저장, 쓰기 지연 SQL 저장소에 SQL 저장
em.persist(member1);
// 1차 캐시에서 조회
Member findMember = em.find(Member.class, 1L);
findMember.setName("박명수");
tx.commit(); // 커밋(쿼리 발생)
}
}
결과
Hibernate:
/* insert com.study.purejpa.entity.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
Hibernate:
/* update
com.study.purejpa.entity.Member */ update
Member
set
name=?
where
id=?
UPDATE
쿼리가 생성된것을 확인 할 수 있습니다.
또한 1차 캐시에서 조회했기 때문에 SELECT
쿼리가 발생되지 않음을 확인 할 수 있습니다.
지연 로딩(lazy loading)
https://tecoble.techcourse.co.kr/post/2022-10-17-jpa-hibernate-proxy/
지연 로딩은 JOIN
과 같은 SQL
에서 주로 활용됩니다.JPA
는 JOIN
되는 엔티티를 함께 조회 하는지 안 하는지에 대한 옵션을 제공합니다.
무슨 말이지? 당연히 조회해야하는거 아냐? 라고 생각할 수 있는데 JPA
는 이러한 것을 옵션으로 제공합니다.
예를 들어, 반드시 Member
를 조회할 때 Team
도 함께 조회해야할까요?
아무리 연관관계가 걸려있다고 해도 Team
과 관련없는 로직을 사용한다면 손해 입니다..
이러한 문제를 JPA
는 지연 로딩을 사용해서 해결합니다.(프록시로 엔티티를 조회하는 방법)
지연 로딩(lazy loading
)은 Member
를 조회할 때 Team
을 조회하지 않습니다.
어떻게 조회하지 않나요? 바로 Team
을 프록시로 조회하기 때문입니다.
그러다가 실제로 애플리케이션에서 Team
을 사용할때 DB
에서 조회하게 됩니다.
이를 코드로 확인해 봅시다.
실험
Member
@Entity
@Getter @Setter
public class Member {
@Id // PK
private Long id;
private String name;
// 지연 로딩 설정
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
public Member() {
}
public Member(Long id, String name, Team team) {
this.id = id;
this.name = name;
this.team = team;
}
}
Team
@Entity
@Getter @Setter
public class Team {
@Id
private Long id;
private String name;
public Team() {
}
public Team(Long id, String name) {
this.id = id;
this.name = name;
}
}
JpaMainV2
public class JpaMainV2 {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team(1L, "홍철없는 홍철팀");
Member member1 = new Member(1L, "유재석", team);
em.persist(team);
em.persist(member1);
em.flush(); // 영속성 컨텍스트 DB 반영
em.clear(); // 영속성 컨텍스트 비움
// 지연 로딩
Member findMember = em.find(Member.class, 1L);
System.out.println("findMember.getTeam()="+ findMember.getTeam().getClass());
tx.commit(); // 커밋
} catch (Exception e) {
tx.rollback(); // 롤백
} finally {
em.close(); // DB Connection을 사용하여 작업함
}
emf.close(); // 애플리케이션이 종료되면 닫아야함
}
}
결과
Hibernate:
select
member0_.id as id1_0_0_,
member0_.name as name2_0_0_,
member0_.team_id as team_id3_0_0_
from
Member member0_
where
member0_.id=?
findMember.getTeam()=class com.study.purejpa.entity.Team$HibernateProxy$rxsaNGou
findMember.getTeam()=class com.study.purejpa.entity.Team$HibernateProxy$rxsaNGou
를 보면 지연로딩 설정으로 인해 프록시 객체를 로딩한것을 알수 있습니다.
그렇다면 아까 언급한 실제로 애플리케이션에서 Team
을 사용할때 DB
에서 조회하게 되는지 확인해 봅시다.
JpaMainV2
public class JpaMainV2 {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team(1L, "홍철없는 홍철팀");
Member member1 = new Member(1L, "유재석", team);
em.persist(team);
em.persist(member1);
em.flush(); // 영속성 컨텍스트 DB 반영
em.clear(); // 영속성 컨텍스트 비움
// 지연 로딩
Member findMember = em.find(Member.class, 1L);
System.out.println("실제로 사용 전 findMember.getTeam()="+ findMember.getTeam().getClass());
System.out.println("실제로 Team 엔티티를 사용하면?");
findMember.getTeam().getName();
tx.commit(); // 커밋
} catch (Exception e) {
tx.rollback(); // 롤백
} finally {
em.close(); // DB Connection을 사용하여 작업함
}
emf.close(); // 애플리케이션이 종료되면 닫아야함
}
}
결과
실제로 Team 엔티티를 사용하면?
Hibernate:
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_
from
Team team0_
where
team0_.id=?
실제로 사용할때 SQL
쿼리가 나가는것을 확인 할수 있습니다...!!
플러시(flush)
지연 로딩(Lazy Loading
)을 설명하기 위해서 직접 flush()
를 호출 한 것을 볼수 있습니다.
왜 호출했을까요? 🤔
영속성 컨텍스트 내용을 DB
에 반영하기 위해서 호출했습니다.
(추가로 em.clear()
를 실행해야 em.find()
를 호출할때 1차 캐시에서 조회하지 않기 때문입니다. em.flush()
만 호출하게 되면 커밋 전까지 1차 캐시에 영속성 컨텍스트로 남아있습니다.)
플러시는 쓰기 지연 SQL 저장소의 쿼리(등록, 수정, 삭제)를 데이터베이스에 전송하는 것을 의미합니다.
다시 말해서 영속성 컨텍스트의 변경 내역을 DB와 동기화하는 것 입니다.
플러시 할 수 있는 방법은 무엇일까요?
- 직접호출
em.flush()
- 플러시 자동 호출
- 트랜잭션 커밋
JPQL
쿼리 실행JPQL
은SQL
로 번역 후 실행되기 때문에 향후 장애 방지(DB에 데이터가 없는 경우)를 위해flush
를 호출합니다.
플러시 모드 옵션은 기본으로 em.setFlushMode(FlushModeType.AUTO)
으로 설정되어 있습니다.
(개발하면서 실제로 변경할 일을 없다고 봐도 무방합니다.)
FlushModeType.AUTO
- 커밋이나 쿼리를 실행할 때 플러시(기본값)
FlushModeType.COMMIT
- 커밋할 때만 플러시
정리
- 영속성 컨텍스트를 비우는것 아님
- 영속성 컨텍스트의 변경내용을 DB에 동기화
- 커밋 직전에만 동기화 하면 된다.(트랜잭션이라는 작업 단위가 중요!)
준영속 상태
영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached
)된 상태를 말합니다.
준영속 상태가 되면 영속성 컨텍스트가 제공하는 기능을 사용하지 못합니다.
준영속 상태 만드는 방법
- 특정 엔티티만 준영속 상태로 전환
em.detach(entity)
- 영속성 컨텍스트를 완전히 초기화
em.clear()
- 영속성 컨텍스트 종료
em.close()
준영속 상태는 주로 테스트 케이스를 작성할 때 사용 합니다.(em.clear()
, em.close()
)
'0+ 스프링 > 0 + 스프링 ORM(JPA)' 카테고리의 다른 글
[JPA] 연관관계 매핑(@ManyToOne, @OneToMany, @OneToOne, @ManyToMany) (0) | 2023.02.20 |
---|---|
[JPA] 연관관계 매핑 개념(패러다임 불일치 해결) (0) | 2023.02.18 |
[JPA] 엔티티 매핑(@Entity, @Table) (0) | 2023.02.16 |
[JPA] JPA 구동 방식과 간단 실습(CRUD) (0) | 2023.02.09 |
[JPA] JPA 기초 (0) | 2023.02.08 |