Updated:

1. 개요

영속성 컨텍스트(Persistence Context)란 Entity를 영구 저장하는 환경이라는 뜻으로 EntityManager를 통해 영속성 컨텍스트에 접근할 수 있다. em.persist()를 하면 DB에 저장되는 것 처럼 보이지만, 실제로는 영속성 컨텍스트에 저장되고, 커밋 시점에 DB에 저장된다. 이번에는 영속성 컨텍스트(Persistence Context)에 대해 알아보도록 하자.

2. 엔티티 생명주기

엔티티 생명주기는 비영속(new/transient), 영속(managed), 준영속(detached), 삭제(removed) 네 가지로 구분할 수 있다.

2-1. 비영속(new/transient)

객체 생성 후 영속성 컨텍스트에 들어가지 않은 상태이다.

1
Member member = new Member("member1");

2-2. 영속(managed)

객체 생성 후 em.persist(entity)를 호출하여 영속성 컨텍스트에 들어간 상태이다.

1
2
Member member = new Member("member1");
em.persist(member);

2-3. 준영속(detached)

영속성 컨텍스트에서 분리되어 나온 상태로, 영속성 컨텍스트의 기능을 사용할 수 없다.

아래 세 가지 방법을 통해 준영속 상태로 만들 수 있다.

  • em.detach(entity) : 준영속 상태로 변환

  • em.clear() : 영속성 컨텍스트 초기화

  • em.close() : 영속성 컨텍스트 종료

1
2
3
4
Member member = new Member("member1");
em.persist(member);

em.detach(member);

2-4. 삭제(removed)

객체를 삭제한 상태이다.

1
2
3
4
Member member = new Member("member1");
em.persist(member);

em.remove(member);

3. 영속성 컨텍스트 특징

3-1. 1차 캐시

1차 캐시는 ID를 키, 엔티티를 값으로하는 맵 형식으로 구성되어있다. 데이터 조회 시 1차 캐시에 데이터가 존재하는 경우 DB에 SELECT 쿼리를 보내는 것이 아니라 1차 캐시에서 데이터를 가져온다. 1차 캐시에 존재하지 않는 데이터를 조회하는 경우에는 DB에서 SELECT 쿼리를 보내 조회하고 1차 캐시에 저장한 후 리턴한다.

1) 1차 캐시에 존재하는 데이터 조회

[소스 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

try {
    Member member = new Member(1L, "member1");
    em.persist(member);

    Member findMember = em.find(Member.class, 1L);
    System.out.println("name = " + findMember.getName());

    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
emf.close();

Line 8 ~ 9 : 아이디가 1인 객체를 영속성 컨텍스트에 저장

Line 11 : 영속성 컨텍스트에 있는 데이터를 조회하므로 DB 쿼리없이 데이터 조회

[실행 결과]

2) 1차 캐시에 존재하지 않는 데이터 조회

[소스 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

try {
    Member findMember = em.find(Member.class, 1L);
    System.out.println("name = " + findMember.getName());

    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
emf.close();

Line 8 : 영속성 컨텍스트에 없는 데이터를 조회하므로 DB 쿼리를 통해 데이터 조회

[실행 결과]

3-2. 동일성 보장

엔티티 조회 요청 시, DB에서 조회하고 조회 결과를 1차 캐시에 저장 후 1차 캐시의 내용을 반환하게 된다. 따라서 같은 엔티티를 여러번 조회 시 1차 캐시에 저장된 하나의 엔티티가 여러번 반환되므로 동일성이 보장된다.

[소스 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

try {
    Member findMember1 = em.find(Member.class, 1L);
    Member findMember2 = em.find(Member.class, 1L);
    System.out.println("findMember1 == findMember2 : " + (findMember1 == findMember2));

    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
emf.close();

Line 8 : 영속성 컨텍스트에 없는 데이터를 조회하므로 DB 쿼리를 통해 데이터 조회

Line 9 : 영속성 컨텍스트에 있는 데이터를 조회하므로 DB 쿼리없이 데이터 조회

[실행 결과]

3-3. 쓰기지연

엔티티 저장 시 바로 DB에 INSERT 쿼리를 보내는 것이 아니라, 영속성 컨텍스트의 쓰기지연 SQL 저장소에 INSERT 쿼리를 저장하고, 1차 캐시에 엔티티를 저장한다. COMMIT 시점에 쓰기지연 SQL 저장소에 있는 쿼리를 한 번에 보내게 된다.

[소스 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

try {
    Member member1 = new Member(1L, "member1");
    Member member2 = new Member(2L, "member2");
    em.persist(member1);
    em.persist(member2);

    System.out.println("=== BEFORE COMMIT() ===");
    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
emf.close();

Line 10 ~ 11 : 1차 캐시와 쓰기지연 SQL 저장소에만 저장되고, INSERT 쿼리 보내지 않음

Line 14 : 커밋 시점에 쓰기지연 SQL 저장소의 쿼리를 DB로 보냄

[실행 결과]

3-4. 변경 감지(Ditry Checking)

영속성 컨텍스트의 1차 캐시에는 스냅샷 컬럼이 존재한다. 1차 캐시 저장 순간의 데이터를 스냅샷에 기록하고, COMMIT 시점에 스냅샷의 엔티티와 현재 엔티티의 상태를 비교하여 변경된 부분이 있으면 자동으로 UPDATE 쿼리를 보낸다.

[소스 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

try {
    Member member = em.find(Member.class, 1L);
    System.out.println("before name : " + member.getName());
    member.setName("member999");
    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
emf.close();

Line 8 : DB에 저장된 데이터를 1차 캐시에 저장

Line 10 : 1차 캐시에 저장된 데이터 수정

[실행 결과]

3-5. 지연 로딩(Lazy Loading)

엔티티 조회 요청 시, 연관관계 매핑이 되어있는 모든 엔티티를 가져오는 것이 아니라, 해당 엔티티만 가져온 후 참조된 객체를 실제로 사용하는 시점에 참조 엔티티를 가져올 수 있다.

4. 플러시

플러시를 하면, 영속성 컨텍스트의 변경내용을 쓰기지연 SQL 저장소에 등록 후 해당 쿼리를 DB에 전송한다. 플러시는 em.flush()를 직접 호출하는 경우, 트랜잭션을 커밋하는 경우, JPQL 쿼리를 실행할 때 발생한다.

4-1. em.flush() 호출

em.flush()를 직접 호출할 수 있다.

[소스 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

try {
    Member member = new Member(1L, "member1");
    em.persist(member);
    em.flush();

    System.out.println("=====================");
    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
emf.close();

Line 9 : 새로 생성한 Member를 1차 캐시에 저장

Line 10 : em.flush()를 직접 호출하여 변경내용 DB 반영

[실행 결과]

4-2. 트랜잭션 커밋

트랜잭션 커밋시점에 em.flush()가 자동으로 호출된다.

[소스 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

try {
    Member member = new Member(1L, "member1");
    em.persist(member);

    System.out.println("=====================");
    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
emf.close();

Line 9 : 새로 생성한 Member를 1차 캐시에 저장

Line 12 : 트랜잭션 커밋

[실행 결과]

4-3. JPQL 쿼리실행

JPQL 쿼리를 실행하면 SQL로 변환하여 DB에 쿼리를 직접 보낸다. 이때 1차 캐시에만 저장되고 DB에 저장되지 않은 데이터가 있는 경우 원하는 결과를 얻지 못할 우려가 있으므로 JPQL 쿼리 시점에 자동으로 em.flush()가 호출된다.

[소스 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();

try {
    Member member = new Member(1L, "member1");
    em.persist(member);

    List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
    System.out.println("=====================");
    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
emf.close();

Line 9 : 새로 생성한 Member를 1차 캐시에 저장

Line 11 : JPQL 쿼리 실행

[실행 결과]

출처 : 자바 ORM 표준 JPA 프로그래밍 - 기본편

Updated:

Leave a comment