Home 영속성 컨텍스트
Post
Cancel

영속성 컨텍스트

EntityManagerFactory와 EntityManager

image

웹 어플리케이션을 개발할 때

EntityManagerFactory를 통해서 고객의 요청이 올때마다 EntityManager를 생성을 하고

EntityManager는 내부적으로 DB 커넥션을 사용해서 DB를 사용하게 된다.





영속성 컨텍스트

  • 영속성 컨텍스트는 논리적인 개념으로 눈에 보이지 않는다.

  • EntityManager를 통해서 영속성 컨텍스트에 접근한다.

  • 영속성 컨텍스트는 Entity를 영구 저장하는 환경이다.

  • EntityManager.persist(entity);

    persist()는 db에 저장하는게 아니라 Entity를 영속성 컨텍스트에 저장한다는 뜻





J2SE 환경

image

엔티티 매니저와 영속성 컨텍스트가 1:1

→ EntityManager를 생성을 하면 그 안에 1:1로 영속성 컨텍스트가 생성이 된다.

→ EntityManager안에 영속성 컨텍스트에 보이지 않는 공간이 생긴다.





엔티티의 생명주기

image

☑️ 비영속 (new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태

→ 최초의 멤버 객체를 생성한 상태


☑️ 영속 (managed) : 영속성 컨텍스트에 관리되는 상태 → persist하고 난 후


☑️ 준영속 (detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태


☑️ 삭제 (removed) : 삭제된 상태





비영속

image

세팅만 한 상태

→ jpa와 관련 없는 상태

1
2
3
4
// 객체를 생성한 상태(비영속) 
Member member = new Member(); 
member.setId("member1"); 
member.setUsername("회원1")



영속

image

객체를 생성한 다음에 EntityManager를 얻어와서 EntityManager에 persist해서 member 객체를 넣으면

EntityManager안에 영속성 컨텍스트에 member 객체가 들어가면서 영속상태가 된다.


em.persist(member); → 이 때 db에 저장되는 것이 아니다. (db 쿼리가 안날라가는 것을 확인할 수 있음)

트랜잭션을 커밋하는 시점에 영속성 컨텍스트에 있던 것이 db에 쿼리가 날라간다.

1
2
3
4
5
6
7
8
9
10
//객체를 생성한 상태(비영속) 
Member member = new Member(); 
member.setId("member1"); 
member.setUsername(회원1);

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

//객체를 저장한 상태(영속)
em.persist(member);



준영속, 삭제

em.detach(member);

영속성 컨텍스트에서 지운다.


em.remove(member);

db 삭제를 요청하는 상태





엔티티 조회, 1차 캐시

image

영속성 컨텍스트는 내부에 1차 캐시를 들고 있다.

*EntityManager와 영속성 컨텍스트는 미묘한 차이가 있지만 여기서는 같다라고 보고 봐도 무방하다.

1
2
3
4
5
6
7
// 엔티티를 생성한 상태(비영속) 
Member member = new Member(); 
member.setId("member1"); 
member.setUsername("회원1");

// 엔티티를 영속 
em.persist(member);

영속성 컨텍스트는 내부에 1차 캐시가 있다.

db의 pk로 매핑한 것이 key값이 되고 Entity 자체가 값이 된다.

→ key : member1, 값 : member 객체



1차 캐시에서 조회

Member 객체를 저장하고 조회를 한다.

em.find()로 조회를 하면 JPA에서는 먼저 영속성 컨텍스트에서 1차 캐시를 찾는다. (db를 먼저 찾아보는 것이 아님)

find에서 member1을 조회한다.



db에서 조회

Member findMember2 = em.find(Member.class, "member2");

member2를 조회를 한다고 가정한다.

image


1. find에서 member2를 조회한다.

2. member2는 1차 캐시에 없으므로 jpa가 db에서 조회를 한다.

3. (db에 member2가 있다는 가정하에) db에서 조회한 member2를 1차 캐시에 저장을 한다.

4. 그리고 member2를 반환한다.


이후에 member2를 다시 조회하게 되면 영속성 컨텍스트 안에 있는 1차 캐시에 있는 member2가 반환된다.



보통 EntityManager는 데이터 트랜잭션 단위로 만들고 데이터 트랜잭션이 끝날 때 영속성 컨텍스트도 종료시킨다.


보통 고객 요청이 들어와서 비즈니스가 끝나버리면 영속성 컨텍스트를 지운다.

1차 캐시도 다 날라가므로 굉장히 짧은 순간에서만 이점이 있다.


application 전체에서 공유하는 캐시는 jpa나 hibernate에서는 2차 캐시라고 한다.

1차 캐시는 db 한 트랜잭션 안에서만 효과가 있기 때문에 성능 이점을 얻을 수 있는 장점은 없다.





엔티티 등록

트랜잭션을 지원하는 쓰기 지연

1
2
3
4
5
6
7
8
9
10
11
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작

em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.(예외도 있긴함)

// 커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋

em.persist(memberA); 로 memberA를 저장을 하면

영속성 컨텍스트 안에는 1차 캐시도 있지만 쓰기 지연 SQL 저장소도 있다.

image



em.persist(memberA);를 실행하면 memberA가 1차 캐시에 들어간다.

동시에 JPA가 INSERT SQL을 생성해서 쓰기 지연 SQL 저장소에 쌓아둔다.

image



transaction.commit();

트랜잭션을 커밋하는 시점에 쓰기 지연 SQL 저장소에 있던 것들이

JPA에서는 flush라고 하는데 flush가 되면서 DB에 쿼리가 날라간다.

image





엔티티 수정

변경 감지(dirty checking)

1
2
3
4
5
6
7
8
9
try {
    // 영속
    // Member member1 = new Member(25L, "A");
    Member member = em.find(Member.class, 25L); // 이전에 저장했던 값 변경하는 것
    member.setName("C");

    System.out.println("==================");
    tx.commit();
}

member.setName() 한 다음에 em.persist(member)라고 작성해야하지 않을까?

예를 들어 자바 컬렉션에서(ex.LIST) 값을 꺼내 변경한 후에는 다시 컬렉션에 집어 넣지 않는다.

따라서 em.persist(member) 코드를 쓰면 안된다. JPA에서는 변경한 값만 작성한다.



image

select 쿼리가 나가고 update 쿼리가 실행되었다.

JPA는 변경감지(Dirty checking)로 인해서 db에 값이 변경된다.

비밀은 영속성 컨텍스트 안에 있다.



image

JPA는 DB 트랜잭션 커밋하는 시점에 내부적으로 flush()가 호출된다.

그리고 엔티티와 스냅샷을 비교한다.


1차 캐시 안에는 pk인 id가 있고 Entity와 스냅샷이 있다.

스냡샷은 내가 값을 읽어 최초로 영속성 컨텍스트에 들어온 그 시점, 1차 캐시에 들어온 상태를 스냅샷으로 떠둔다.


그리고 값(memberA)을 변경했을 때 JPA에서 트랜잭션 커밋되는 시점에 내부적으로 flush()가 호출되면서 비교를 한다.

Entity와 스냅샷을 비교해서 memberA가 바뀐 것을 알고 UPDATE 쿼리문을 쓰기 지연 SQL 저장소에 만들어 둔다.

UPDATE 쿼리문 db에 반영을 하고 commit을 한다. 👉🏻 이것을 변경 감지라고 한다.



실제 내가 프로젝트에서 작성한 코드로 보면 게시글을 작성하는 코드에서는

registryRepository.save(registry);를 작성한 것과 다르게 updateArticle에서는 save가 없다.

*일부 코드 제거하고 필요한 코드만 작성함

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   // Service.java
    @Override
    @Transactional
    public Registry updateArticle(Long id, RegistryRequestDto requestDto){
        Registry registry = registryRepository.findById(id).orElseThrow(RegistryNotFoundException::new);
        return registry.updateRegistry(requestDto);
    }

   // Registry.java
    public void updateRegistry(RegistryRequestDto requestDto){
        if(requestDto.getTitle() != null){
            this.title = requestDto.getTitle();
        }
        if(requestDto.getMain() != null){
            this.main = requestDto.getMain();
        }
    }

JPA 변경감지가 작동되기 위한 조건 중에 Entity가 영속상태여야 하고 트랜잭션 안에 묶여 있어야 한다.

JPA를 사용해서 트랜잭션을 관리하는 방법은 @Transactional을 사용하는 방법이다.

따라서 @Transactional을 작성해야한다.


스프링은 @Transactional을 사용하면

PlatformTransactionManager를 구현한 JpaTransactionManager를 사용해서 트랜잭션을 관리한다.





스프링 컨테이너의 기본 전략

스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.

  • 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다.

  • 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다.

  • 같은 트랜잭션 안에서는 여러 위치(여러 repository)의 EntityManager를 사용해도 항상 같은 영속성 컨텍스트에 접근한다.


다양한 위치에서 EntityManager를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용한다.

트랜잭션이 다르면 동일한 엔티티 매니저를 사용해도 다른 영속성 컨텍스트를 사용한다.





This post is licensed under CC BY 4.0 by the author.