영속성 컨텍스트란?
영속성 컨텍스트는 JPA의 이해에 있어 가장 중요한 용어이다. 엔티티를 영구 저장하는 환경이라는 뜻을 가진 논리적인 개념인데, EntityManager를 통해 접근할 수 있다.
EntityManger는 EntityManagerFactory에서 사용자의 요청 하나당 하나씩 생성해 DB 커넥션풀을 통해 DB에 대한 CRUD를 가능하게 하는데, EntityManagerFactory는 웹 어플리케이션 하나 당 하나만 존재할 수 있고, EntityManager는 하나의 트랜잭션 당 하나씩 존재할 수 있다. 이 말은, 멀티 스레드간 EntityManager의 공유는 불가능하다는 의미이다.
그리고 이 영속성 컨텍스트는 환경별로 EntityManager와의 관계에 있어 차이가 발생하는데, 아래와 같다.
- J2SE : EntityManager : PersistenceContext = 1 : 1
- J2EE, SpringFramework : EntityManager : PersistenceContext : N : 1
엔티티의 생명주기
- 비영속 (new / transient)
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태 - 영속 (managed)
영속성 컨텍스트에 의해 관리되는 상태 - 준영속 (detached)
영속성 컨텍스트에 저장되었다가 분리된 상태 - 삭제 (removed)
삭제된 상태
비영속 상태
비영속 상태는 영속성 컨텍스트와 엔티티 객체가 전혀 관계가 없는 상태를 의미한다.
예를 들어, 아래 코드와 같이 새로운 Member 객체를 생성했을 때 이 객체는 영속성 컨텍스트와 전혀 관련이 없으며, 이 객체를 대상으로 setName()하거나 객체를 제거하는 등 변경사항이 발생해도 DB에는 아무런 변화가 없다.
1
2
3
4
5
6
7
8
9
10
11
|
EntityManagerFactory factory = Persistence.createEntityManagerFactory("hello");
EntityManager manager = factory.createEntityManager();
EntityTransaction transaction = manager.getTransaction();
transaction.begin();
Member member = new Member();
member.setId(100L);
member.setName("비영속");
transaction.commit();
|
cs |
member가 영속 상태라면 transaction.commit() 수행 시 UPDATE 쿼리문이 작성되어 실행되야하지만, 현재 비영속 상태이므로 아무런 동작도 발생하지 않는다.
영속 상태
영속 상태는 엔티티 객체가 영속성 컨텍스트에 의해 관리되고 있는 상태를 의미한다.
때문에, 객체에 변경사항이 발생하면 영속성 컨텍스트가 변경을 감지하고 트랜잭션의 commit이 발생하면 적당한 SQL문을 작성해 DB로 전송한다.
위 비영속 상태의 엔티티를 영속 상태로 변경해주려면 EntityManager.persist() 메서드를 호출해주면된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
EntityManagerFactory factory = Persistence.createEntityManagerFactory("hello");
EntityManager manager = factory.createEntityManager();
EntityTransaction transaction = manager.getTransaction();
transaction.begin();
Member member = new Member();
member.setId(100L);
member.setName("영속");
System.out.println("=============== before persist ===============");
// 영속 상태로 변경
manager.persist(member);
System.out.println("=============== after persist ===============");
transaction.commit();
|
cs |
하지만, 엔티티 객체를 영속 상태로 변경한다고 해서 그 즉시 SQL문이 작성되어 DB로 전송되는 것은 아니다. 위 코드를 실행하면 Console에 아래와 같은 결과가 출력된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
D:\Java\jdk1.8.0_151\bin\java.exe "-javaagent:D:\Programs\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=9041:D:\Programs\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\Java\jdk1.8.0_151\jre\lib\charsets.jar;D:\Java\jdk1.8.0_151\jre\lib\deploy.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\access-bridge-64.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\cldrdata.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\dnsns.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\jaccess.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\jfxrt.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\localedata.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\nashorn.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\sunec.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\sunjce_provider.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\sunmscapi.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\sunpkcs11.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\zipfs.jar;D:\Java\jdk1.8.0_151\jre\lib\javaws.jar;D:\Java\jdk1.8.0_151\jre\lib\jce.jar;D:\Java\jdk1.8.0_151\jre\lib\jfr.jar;D:\Java\jdk1.8.0_151\jre\lib\jfxswt.jar;D:\Java\jdk1.8.0_151\jre\lib\jsse.jar;D:\Java\jdk1.8.0_151\jre\lib\management-agent.jar;D:\Java\jdk1.8.0_151\jre\lib\plugin.jar;D:\Java\jdk1.8.0_151\jre\lib\resources.jar;D:\Java\jdk1.8.0_151\jre\lib\rt.jar;D:\workspace_jpa\ex1-hello-jpa\target\classes;C:\Users\chiwo\.m2\repository\org\hibernate\hibernate-core\5.3.10.Final\hibernate-core-5.3.10.Final.jar;C:\Users\chiwo\.m2\repository\org\jboss\logging\jboss-logging\3.3.2.Final\jboss-logging-3.3.2.Final.jar;C:\Users\chiwo\.m2\repository\javax\persistence\javax.persistence-api\2.2\javax.persistence-api-2.2.jar;C:\Users\chiwo\.m2\repository\org\javassist\javassist\3.23.2-GA\javassist-3.23.2-GA.jar;C:\Users\chiwo\.m2\repository\net\bytebuddy\byte-buddy\1.9.5\byte-buddy-1.9.5.jar;C:\Users\chiwo\.m2\repository\antlr\antlr\2.7.7\antlr-2.7.7.jar;C:\Users\chiwo\.m2\repository\org\jboss\spec\javax\transaction\jboss-transaction-api_1.2_spec\1.1.1.Final\jboss-transaction-api_1.2_spec-1.1.1.Final.jar;C:\Users\chiwo\.m2\repository\org\jboss\jandex\2.0.5.Final\jandex-2.0.5.Final.jar;C:\Users\chiwo\.m2\repository\com\fasterxml\classmate\1.3.4\classmate-1.3.4.jar;C:\Users\chiwo\.m2\repository\javax\activation\javax.activation-api\1.2.0\javax.activation-api-1.2.0.jar;C:\Users\chiwo\.m2\repository\org\dom4j\dom4j\2.1.1\dom4j-2.1.1.jar;C:\Users\chiwo\.m2\repository\org\hibernate\common\hibernate-commons-annotations\5.0.4.Final\hibernate-commons-annotations-5.0.4.Final.jar;C:\Users\chiwo\.m2\repository\org\mariadb\jdbc\mariadb-java-client\2.6.0\mariadb-java-client-2.6.0.jar hellojpa.JpaMainEx3
6월 07, 2020 4:17:02 오후 org.hibernate.jpa.internal.util.LogHelper logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [
name: hello
...]
6월 07, 2020 4:17:03 오후 org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {5.3.10.Final}
6월 07, 2020 4:17:03 오후 org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
6월 07, 2020 4:17:03 오후 org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
6월 07, 2020 4:17:05 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
WARN: HHH10001002: Using Hibernate built-in connection pool (not for production use!)
6월 07, 2020 4:17:05 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001005: using driver [org.mariadb.jdbc.Driver] at URL [jdbc:mariadb://localhost:3306/jpadb]
6월 07, 2020 4:17:05 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001001: Connection properties: {user=root, password=****}
6월 07, 2020 4:17:05 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001003: Autocommit mode: false
6월 07, 2020 4:17:05 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PooledConnections <init>
INFO: HHH000115: Hibernate connection pool size: 20 (min=1)
6월 07, 2020 4:17:05 오후 org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MariaDBDialect
=============== before persist ===============
=============== after persist ===============
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
6월 07, 2020 4:17:07 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:mariadb://localhost:3306/jpadb]
Process finished with exit code 0
|
cs |
실제 SQL문이 작성되고 수행되는 것은 트랜잭션이 commit 되었을 때이기 때문이다.
준영속 상태
준영속 상태는 엔티티 객체가 영속성 컨텍스트에서 분리된 상태를 의미한다.
준영속 상태의 엔티티 객체는 변경이 발생해도 DB에 영향을 주지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
EntityManagerFactory factory = Persistence.createEntityManagerFactory("hello");
EntityManager manager = factory.createEntityManager();
EntityTransaction transaction = manager.getTransaction();
transaction.begin();
try
{
Member member = manager.find(Member.class, 100L);
member.setName("영속상태");
// member 엔티티를 준영속 상태로 지정
manager.detach(member);
// 커밋 시 select 문만 수행되고 update 문은 수행안됨
transaction.commit();
}
catch (Exception e)
{
transaction.rollback();
}
finally
{
manager.close();
}
factory.close();
|
cs |
위 코드에서는 id가 100인 Member를 조회해 준영속 상태로 변경한다. member 객체가 준영속 상태로 변경되기 전에 setName("영속상태"); 코드로 name 컬럼의 값이 변경되었지만 commit 이전에 준영속 상태로 변경되었기 때문에 이 코드를 수행하면 SELECT 쿼리만 작성되고 수행된다.
영속성 컨텍스트의 이점
그렇다면 영속성 컨텍스트의 이점에는 무엇이 있을까?
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지 (Dirty Checking)
- 지연 로딩 (Lazy Loading)
1차 캐시
영속성 컨텍스트에 엔티티가 영속이 되면 실제 DB에 영속된 엔티티에 대한 데이터가 없더라도 1차 캐시에서 데이터가 조회된다. 또, 동일한 엔티티에 대한 find가 중복해 발생할 경우 첫번째 find에서 DB에서 데이터를 직접 가져오고 이후 find에서는 1차 캐시에서 데이터를 가져온다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
EntityManagerFactory factory = Persistence.createEntityManagerFactory("hello");
EntityManager manager = factory.createEntityManager();
EntityTransaction transaction = manager.getTransaction();
transaction.begin();
try
{
// 비영속 상태
Member member = new Member();
member.setId(100L);
member.setName("비영속");
// 영속
manager.persist(member);
// 1차 캐시를 통해 DB에 저장되지 않은 데이터를 조회
Member findMember = manager.find(Member.class, 100L);
System.out.println("findMember ID : " + findMember.getId());
System.out.println("findMember NAME : " + findMember.getName());
transaction.commit();
}
catch (Exception e)
{
transaction.rollback();
}
finally
{
manager.close();
}
factory.close();
|
cs |
위 코드를 수행하면, manager.persist(member); 코드로 인한 INSERT 쿼리만 수행된다. 18 line의 findMember는 1차 캐시를 통해 반환되기 때문이다.
동일성 보장
아래 코드는 동일한 id를 가진 데이터를 2회 조회하는 코드인데, 1차 캐시 덕분에 SELECT 쿼리가 한 번만 수행되고, 2개의 변수가 동일성이 보장되어 같은 객체로 인식된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
EntityManagerFactory factory = Persistence.createEntityManagerFactory("hello");
EntityManager manager = factory.createEntityManager();
EntityTransaction transaction = manager.getTransaction();
transaction.begin();
try
{
System.out.println("=============== find 1 ===============");
Member member1 = manager.find(Member.class, 100L);
System.out.println("=============== find 2 ===============");
// 1차 캐시로 인해 select 쿼리가 수행되지 않음
Member member2 = manager.find(Member.class, 100L);
System.out.println("=============== end ===============");
// 영속 엔티티의 동일성 보장
System.out.println(member1 == member2); // true
transaction.commit();
}
catch (Exception e)
{
transaction.rollback();
}
finally
{
manager.close();
}
factory.close();
|
cs |
트랜잭션을 지원하는 쓰기 지연
영속성 컨텍스트는 엔티티가 변경되거나 영속성이 추가되었을 때 즉시 SQL을 작성하지 않고 트랜잭션의 커밋이 발생할 때 모두 모아서 한 번에 SQL을 작성하고 DB로 전송한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
EntityManagerFactory factory = Persistence.createEntityManagerFactory("hello");
EntityManager manager = factory.createEntityManager();
EntityTransaction transaction = manager.getTransaction();
transaction.begin();
try
{
//쓰기지연 -> commit 시에 insert 쿼리 2개가 수행됨
Member memberA = new Member();
memberA.setId(110L);
memberA.setName("MemberA");
Member memberB = new Member();
memberB.setId(120L);
memberB.setName("MemberB");
System.out.println("======== before persist A ========");
manager.persist(memberA);
System.out.println("======== before persist B ========");
manager.persist(memberB);
System.out.println("======== after persist ========");
transaction.commit();
}
catch (Exception e)
{
transaction.rollback();
}
finally
{
manager.close();
}
factory.close();
|
cs |
위 코드를 수행하면 아래 결과가 Console에 출력된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
D:\Java\jdk1.8.0_151\bin\java.exe "-javaagent:D:\Programs\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=12934:D:\Programs\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\Java\jdk1.8.0_151\jre\lib\charsets.jar;D:\Java\jdk1.8.0_151\jre\lib\deploy.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\access-bridge-64.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\cldrdata.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\dnsns.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\jaccess.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\jfxrt.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\localedata.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\nashorn.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\sunec.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\sunjce_provider.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\sunmscapi.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\sunpkcs11.jar;D:\Java\jdk1.8.0_151\jre\lib\ext\zipfs.jar;D:\Java\jdk1.8.0_151\jre\lib\javaws.jar;D:\Java\jdk1.8.0_151\jre\lib\jce.jar;D:\Java\jdk1.8.0_151\jre\lib\jfr.jar;D:\Java\jdk1.8.0_151\jre\lib\jfxswt.jar;D:\Java\jdk1.8.0_151\jre\lib\jsse.jar;D:\Java\jdk1.8.0_151\jre\lib\management-agent.jar;D:\Java\jdk1.8.0_151\jre\lib\plugin.jar;D:\Java\jdk1.8.0_151\jre\lib\resources.jar;D:\Java\jdk1.8.0_151\jre\lib\rt.jar;D:\workspace_jpa\ex1-hello-jpa\target\classes;C:\Users\chiwo\.m2\repository\org\hibernate\hibernate-core\5.3.10.Final\hibernate-core-5.3.10.Final.jar;C:\Users\chiwo\.m2\repository\org\jboss\logging\jboss-logging\3.3.2.Final\jboss-logging-3.3.2.Final.jar;C:\Users\chiwo\.m2\repository\javax\persistence\javax.persistence-api\2.2\javax.persistence-api-2.2.jar;C:\Users\chiwo\.m2\repository\org\javassist\javassist\3.23.2-GA\javassist-3.23.2-GA.jar;C:\Users\chiwo\.m2\repository\net\bytebuddy\byte-buddy\1.9.5\byte-buddy-1.9.5.jar;C:\Users\chiwo\.m2\repository\antlr\antlr\2.7.7\antlr-2.7.7.jar;C:\Users\chiwo\.m2\repository\org\jboss\spec\javax\transaction\jboss-transaction-api_1.2_spec\1.1.1.Final\jboss-transaction-api_1.2_spec-1.1.1.Final.jar;C:\Users\chiwo\.m2\repository\org\jboss\jandex\2.0.5.Final\jandex-2.0.5.Final.jar;C:\Users\chiwo\.m2\repository\com\fasterxml\classmate\1.3.4\classmate-1.3.4.jar;C:\Users\chiwo\.m2\repository\javax\activation\javax.activation-api\1.2.0\javax.activation-api-1.2.0.jar;C:\Users\chiwo\.m2\repository\org\dom4j\dom4j\2.1.1\dom4j-2.1.1.jar;C:\Users\chiwo\.m2\repository\org\hibernate\common\hibernate-commons-annotations\5.0.4.Final\hibernate-commons-annotations-5.0.4.Final.jar;C:\Users\chiwo\.m2\repository\org\mariadb\jdbc\mariadb-java-client\2.6.0\mariadb-java-client-2.6.0.jar hellojpa.JpaMainEx3
6월 07, 2020 4:47:08 오후 org.hibernate.jpa.internal.util.LogHelper logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [
name: hello
...]
6월 07, 2020 4:47:09 오후 org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {5.3.10.Final}
6월 07, 2020 4:47:09 오후 org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
6월 07, 2020 4:47:09 오후 org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
6월 07, 2020 4:47:11 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
WARN: HHH10001002: Using Hibernate built-in connection pool (not for production use!)
6월 07, 2020 4:47:11 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001005: using driver [org.mariadb.jdbc.Driver] at URL [jdbc:mariadb://localhost:3306/jpadb]
6월 07, 2020 4:47:11 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001001: Connection properties: {user=root, password=****}
6월 07, 2020 4:47:11 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001003: Autocommit mode: false
6월 07, 2020 4:47:11 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PooledConnections <init>
INFO: HHH000115: Hibernate connection pool size: 20 (min=1)
6월 07, 2020 4:47:11 오후 org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MariaDBDialect
======== before persist A ========
======== before persist B ========
======== after persist ========
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
6월 07, 2020 4:47:13 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:mariadb://localhost:3306/jpadb]
Process finished with exit code 0
|
cs |
이는 쓰기 지연에 의한 결과인데, 각각의 persist 메서드 호출 시점에 INSERT SQL을 생성해서 쓰기 지연 SQL 저장소에 저장하고, 트랜잭션의 commit 호출 시 한 번에 DB로 flush 하기 때문이다. 이 과정을 도식으로 표현하면 아래와 같다.
변경 감지 (Dirty Checking)
영속성 컨텍스트는 영속 엔티티에 대한 변경을 감지한다. 영속 엔티티에 대한 스냅샷을 갖고 있기 때문인데, 덕분에 영속엔티티에 대한 변경이 발생한 후에 별도의 update나 persist 같은 코드를 작성하지 않아도 트랜잭션이 commit되면 자동으로 변경을 감지하고 적절한 SQL을 작성해 수행한다.
출처 :: 인프런 강의(자바 ORM 표준 JPA 프로그래밍 - 기본편)
'🌱 SPRING > JPA' 카테고리의 다른 글
[JPA] 엔티티 (0) | 2020.06.13 |
---|---|
[JPA] 플러시(flush) (0) | 2020.06.13 |
[JPA] JPQL 맛보기 (0) | 2020.06.07 |
[JPA] JPA로 CRUD 작성하기 (0) | 2020.06.06 |
[JPA] Dialect(방언) (0) | 2020.06.06 |