2024. 8. 13. 15:04ㆍ스프링
2024/08/13
※ 영속성 컨텍스트에 대해 알아보자.
▶ 영속성 컨텍스트란?
● Persistence를 한글로 번역하면 '영속성, 지속성'이라는 뜻이다.
Persistence를 객체의 관점으로 해석해 보자면 ‘객체가 생명(객체가 유지되는 시간)이나 공간(객체의 위치)을 자유롭게 유지하고 이동할수 있는 객체의 성질’을 의미한다.
● 좀 더 쉽게 표현해 보자면 Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간이라고 할 수 있다.
● 직접 SQL을 작성하지 않아도 JPA를 사용하여 DB에 데이터를 저장 or 조회할 수 있으며 수정, 삭제 또한 가능
● 이러한 일련의 과정을 효율적으로 처리하기 위해 JPA는 영속성 컨텍스트에 Entity 객체들을 저장하여 관리하면서 DB와 소통한다.
▶ Entity Manager
● 영속성 컨텍스트에 접근하여 Entity 객체들을 조작하기 위해서는 EntityManager가 필요하다.
EntityManager는 이름 그대로 Entity를 관리하는 관리자입니다.
● 개발자들은 EntityManager를 사용해서 Entity를 저장하고 조회하고 수정하고 삭제할 수 있다.
● EntityManager는 EntityManagerFactory를 통해 생성하여 사용할 수 있습니다.
▶ EntityManagerFactory
● EntityManagerFactory는 일반적으로 DB 하나에 하나만 생성되어 애플리케이션이 동작하는 동안 사용된다.
● EntityManagerFactory를 만들기 위해서는 DB에 대한 정보를 전달해야 한다.
○ 정보를 전달하기 위해서는 /resources/META-INF/ 위치에 persistence.xml 파일을 만들어 정보를 넣어두면 된다.
📌 persistence.xml 파일 예시
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="memo">
<class>com.sparta.entity.Memo</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="{비밀번호}"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/{연결테이블명}"/>
<property name="hibernate.hbm2ddl.auto" value="create" />
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
</properties>
</persistence-unit>
</persistence>
EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
EntityManager em = emf.createEntityManager();
● EntityManagerFactory emf = Persistence.createEntityManagerFactory("{테이블이름}");
● 해당 코드를 호출하면 JPA는 persistence.xml 의 정보를 토대로 EntityManagerFactory를 생성한다.
● EntityManager em = emf.createEntityManager(); 코드를 호출하면 EntityManagerFactory를 사용하여 EntityManager를 생성할 수 있다.
▶ 영속성 컨텍스트의 기능
📌 영속성 컨텍스트는 Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간이다.
영속성 컨텍스트가 어떻게 Entity 객체를 효율적으로 관리하고 있는지 살펴보자.
▼ 1차 캐시
● 영속성 컨텍스트는 내부적으로 캐시 저장소를 가지고 있다.
○ 우리가 저장하는 Entity 객체들이 1차 캐시 즉, 캐시 저장소에 저장된다.
○ 캐시 저장소는 Map 자료구조 형태로 되어있다.
✅ key에는 @Id로 매핑한 기본 키 즉, 식별자 값을 저장한다.
✅ value에는 해당 Entity 클래스의 객체를 저장한다.
✅ 영속성 컨텍스트는 캐시 저장소 Key에 저장한 식별자값을 사용하여 Entity 객체를 구분하고 관리한다.
🔎 영속성 컨텍스트가 이 캐시 저장소를 어떻게 활용하고 있는지 살펴보자.
👉 Entity 저장
● em.persist(memo); 메서드가 호출되면 memo Entity 객체를 캐시 저장소에 저장한다.
○ 예시) 메모 저장
@Test
@DisplayName("1차 캐시 : Entity 저장")
void test1() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = new Memo();
memo.setId(1L);
memo.setUsername("Robbie");
memo.setContents("1차 캐시 Entity 저장");
em.persist(memo);
et.commit();
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
○ em > persistenceContext > entitiesBykey를 확인해 보면 key-value 형태로 정보가 저장되어있음을 확인할 수 있다.
👉 Entity 조회
1. 캐시 저장소에 조회하는 Id가 존재하지 않은 경우
a. 캐시 저장소 조회
b. DB SELECT 조회 후 캐시 저장소에 저장
● em.find(Memo.class, 1); 호출 시 캐시 저장소를 확인 한 후 해당 값이 없다면?
● DB에 SELECT 조회 후 해당 값을 캐시 저장소에 저장하고 반환한다.
○ 예시) memo 조회
@Test
@DisplayName("Entity 조회 : 캐시 저장소에 해당하는 Id가 존재하지 않은 경우")
void test2() {
try {
Memo memo = em.find(Memo.class, 1);
System.out.println("memo.getId() = " + memo.getId());
System.out.println("memo.getUsername() = " + memo.getUsername());
System.out.println("memo.getContents() = " + memo.getContents());
} catch (Exception ex) {
ex.printStackTrace();
} finally {
em.close();
}
emf.close();
}
➡️ DB에서 데이터를 조회만 하는 경우에는 데이터의 변경이 발생하는 것이 아니기 때문에
트랜잭션이 없어도 조회가 가능하다.
➡️ Memo memo = em.find(Memo.class, 1); 호출 시 캐시 저장소에 해당 값이 존재하지 않기 때문에
DB에 SELECT 조회하여 캐시 저장소에 저장한 후 반환한다.
1. 캐시 저장소에 조회하는 Id가 존재하는 경우
● em.find(Memo.class, 1); 호출 시 캐시 저장소에 식별자 값이 1이면서 Memo Entity 타입인 값이 있는지 조회한다.
○ 값이 있다면 해당 Entity 객체를 반환한다.
💡 '1차 캐시' 사용의 장점
1. DB 조회 횟수를 줄임
2. '1차 캐시'를 사용해 DB row 1개 당 객체 1개가 사용되는 것을 보장(객체 동일성 보장)
📌 객체 동일성 보장
@Test @DisplayName("객체 동일성 보장") void test4() { EntityTransaction et = em.getTransaction(); et.begin(); try { Memo memo3 = new Memo(); memo3.setId(2L); memo3.setUsername("Robbert"); memo3.setContents("객체 동일성 보장"); em.persist(memo3); Memo memo1 = em.find(Memo.class, 1); Memo memo2 = em.find(Memo.class, 1); Memo memo = em.find(Memo.class, 2); System.out.println(memo1 == memo2); System.out.println(memo1 == memo); et.commit(); } catch (Exception ex) { ex.printStackTrace(); et.rollback(); } finally { em.close(); } emf.close(); }
● 같은 값을 조회하는 memo1과 memo2는 == 결과 true를 반환한다.
● memo1과 다른 값을 조회하는 memo는 == 결과 false를 반환한다.
👉 Entity 삭제
1. 삭제할 Entity를 조회한 후 캐시 저장소에 없다면 DB에 조회해서 저장한다.
2. em.remove(entity);
● em.remove(memo); 호출 시 삭제할 Entity를 DELETED 상태로 만든 후 트랜잭션 commit 후 Delete SQL이 DB에 요청다.
○ 예시) memo 삭제
@Test
@DisplayName("Entity 삭제")
void test5() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = em.find(Memo.class, 2);
em.remove(memo);
et.commit();
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
➡️ em.find(Memo.class, 2); 호출되면서 memo Entity 객체를 DELETED 상태로 만들고 트랜잭션 commit 후 Delete SQL이 DB에 요청되었다.
➡️ em.find(Memo.class, 2); 호출 하여 memo 객체를 캐시 저장소에 저장한 후
➡️ entityEntry를 확인해보면 memo Entity 객체가 영속성 컨텍스트가 관리하는 MANAGED 상태인 것을 확인할 수 있다.
➡️ em.remove(memo); 호출 후 memo Entity 객체가 DELETED 상태로 바뀐 것을 확인할 수 있다.
➡️ 트랜잭션 commit 후 DB 데이터를 확인해보면 해당 데이터가 삭제 되어있는 것을 확인할 수 있다.
▼ 쓰기 지연 저장소(ActionQueue)
● JPA는 트랜잭션처럼 SQL을 모아서 한번에 DB에 반영한다.
○ JPA는 이를 구현하기 위해 쓰기 지연 저장소를 만들어 SQL을 모아두고 있다가 트랜잭션 commit 후 한번에 DB에 반영한다.
● Debugging을 통해 실제로 쓰기 지연 저장소에 SQL을 모아서 한번에 반영하는지 확인해보자.
👉 쓰기 지연 저장소 (ActionQueue)
@Test
@DisplayName("쓰기 지연 저장소 확인")
void test6() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = new Memo();
memo.setId(2L);
memo.setUsername("Robbert");
memo.setContents("쓰기 지연 저장소");
em.persist(memo);
Memo memo2 = new Memo();
memo2.setId(3L);
memo2.setUsername("Bob");
memo2.setContents("과연 저장을 잘 하고 있을까?");
em.persist(memo2);
System.out.println("트랜잭션 commit 전");
et.commit();
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
● 쓰기 지연 저장소 확인
○ em > actionQueue를 확인해보면 insertions > executables에 Insert할 memo#2, memo#3 Entity 객체 2개가 들어가 있는 것을 확인할 수 있다.
● 트랜잭션 commit 후
○ actionQueue에 있던 insertions 데이터가 사라진 것을 확인할 수 있다.
○ 실제로 기록을 확인해보면 트랜잭션 commit 호출 전까지는 SQL 요청이 없다가 트랜잭션 commit 후 한번에 Insert SQL 2개가 순서대로 요청된 것을 확인할 수 있다.
▼ 변경 감지(Dirty Checking)
● 영속성 컨텍스트에 저장된 Entity가 변경될 때마다 Update SQL이 쓰기 지연 저장소에 저장된다면❓
○ 하나의 Update SQL로 처리할 수 있는 상황을 여러번 Update SQL을 요청하게 되기 때문에 비효율적
● 그렇다면 JPA는 어떻게 Update를 처리할까❓
○ em.update(entity); 같은 메서드를 지원할 것 같지만 찾아볼 수 없다❗
● JPA는 영속성 컨텍스트에 Entity를 저장할 때 최초 상태(LoadedState)를 저장한다.
○ 트랜잭션이 commit되고 em.flush(); 가 호출되면 Entity의 현재 상태와 저장한 최초 상태를 비교합.
○ 변경 내용이 있다면 Update SQL을 생성하여 쓰기 지연 저장소에 저장하고 모든 쓰기지연 저장소의 SQL을 DB에 요청한다.
○ 마지막으로 DB의 트랜잭션이 commit 되면서 반영된다.
● 따라서 변경하고 싶은 데이터가 있다면 먼저 데이터를 조회하고 해당 Entity 객체의 데이터를 변경하면 자동으로 Update SQL이 생성되고 DB에 반영된다.
○ 이러한 과정을 변경 감지, Dirty Checking이라 부른다.
오늘은 영속성 컨텍스트의 개념과 기능에 대해 알아보았습니다.
※ 위 이미지들은 스파르타코딩클럽에 저작권이 있으므로 무단 도용 금지 및 상업 목적으로 사용할 수 없습니다.
'스프링' 카테고리의 다른 글
스프링 - Spring Security 프레임워크 (0) | 2024.08.14 |
---|---|
스프링 - Filter란? (0) | 2024.08.14 |
스프링 - Entity란? (0) | 2024.08.12 |
스프링 - Spring Data JPA란 무엇일까? (0) | 2024.08.09 |
스프링 - IoC Container와 Bean (0) | 2024.08.08 |