오라클 공식문서에서는 영속성 컨텍스트에 대해 다음과 같이 정의하고 있다.

A persistence context is a set of entity instances in which for any persistent entity identity there is a unique entity instance. Within the persistence context, the entity instances and their lifecycle are managed.

1. 각각의 영속적인 엔티티 식별자에 대해 유일한 엔티티 인스턴스가 있는 엔티티 인스턴스의 집합이다.
2. 영속성 컨텍스트 내에서, 엔티티 인스턴스들과 그들의 생명주기가 관리된다.

1. 의 내용을 풀어서 말하면

컨텍스트 내에서 엔티티끼리 고유하게 식별하게 하는 id를 가지고, 동일한 id를 가진 엔티티가 존재할 수 없다.

JdbcTemplate을 사용하던 시절로 돌아가 다음 코드를 보자. 

@Test  
void findById() {  
	Member insert = memberDao.insert(new Member("maco", "pass"));  
	  
	Member member1 = memberDao.findById(insert.getId()).get();  
	Member member2 = memberDao.findById(insert.getId()).get();  
	  
	System.out.println(member1);  
	System.out.println(member2);  
	System.out.println(member1 == member2);
}

 

위 코드에서 member1과 member2는 같은 insert.getId()를 이용하여 DB를 조회했기 때문에 같은 데이터를 가진다.

그렇다면 member1과 member2는 같은 객체인가?

출력 결과를 확인해보자.

cart.entity.Member@194c64c9
cart.entity.Member@3a64b5d
false

DB에서 id를 가진 row를 조회하여 같은 데이터를 가지고 있으나 DAO가 객체를 생성할 때 new 오퍼레이션을 사용하여 생성했기 때문에 객체가 동일하지는 않다.

 

1차 캐시

영속성 컨텍스트는 1차 캐시를 제공하여 위 예시에서 설명한 동일성 문제를 해결함과 동시에 성능 최적화를 지원한다.

  1. 같은 객체를 반환하여 동일성 보장하고 식별자 마다 유일한 엔티티를 보장
  2. 캐싱으로 같은 조회라면 쿼리를 실행하지 않아 성능을 최적화한다.

1차 캐시를 이해하기 위해 다음 그림을 보자.

영속성 컨텍스트에서 조회 요청이 들어왔을 때, 1차 캐시에 캐싱된 엔티티가 있다면 DB를 조회하지 않고 바로 캐싱된 엔티티를 반환한다. 

캐시는 데이터베이스 기본키의 값을 키로, 엔티티를 값으로 매핑하여 저장하고 있다. 엔티티의 식별자인 기본 키를 키로 매핑하기 때문에 식별자마다 엔티티의 유일함을 보장한다.

 

엔티티 생명 주기

글의 맨 처음 영속성 컨텍스트의 정의 부분에서 영속성 컨텍스트는 엔티티의 생명 주기를 관리한다고 했다.

엔티티에는 4가지 상태가 존재한다.

  • 비영속 : 영속성 컨텍스트와 관계가 없는 상태
  • 영속 : 영속성 컨텍스트에 저장된 상태
  • 준영속 : 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제 : 삭제된 상태

영속성 컨텍스트는 엔티티 매니저를 통해 이러한 엔티티의 생명 주기를 관리한다.

코드를 보며 이해해보자.

@Transactional
@Rollback(value = false)
@SpringBootTest
class UserTest {

    @PersistenceContext
    private EntityManager entityManager;

    @Test
    void test() {
        User user = new User();    // 비영속
        user.setUsername("maco");    // 비영속
        entityManager.persist(user);    // 영속
    }
}

 

엔티티 매니저를 의존성 주입 받기 위해 @SpringBootTest로 작성했다.

또한, 트랜잭션 내에서 관리해야 하기 때문에 @Transactional을 붙여주고, DB에 데이터가 플러시 되는 것을 직접 확인하기 위해 @Rollback(value = false)로 설정했다.

위 코드에서 엔티티 매니저의 persist를 호출하기 전에 user는 비영속 상태였다가 persist를 호출하면 영속성 컨텍스트에서 관리하기 때문에 영속되었다고 말한다.

위에서 봤던 1차 캐시 그림에서 데이터를 조회하면 영속성 컨텍스트의 1차 캐시 안에 저장되었다.

즉, 조회도 영속성 컨텍스트가 엔티티를 관리하기 때문에 조회를 하면 그 엔티티는 영속 상태라고 한다.

    @Test
    void test() {
        User user = entityManager.find(User.class, 15L);	// 영속
        System.out.println("user = " + user);
    }

 

entityManager.find()를 통해 조회한 user는 영속성 컨텍스트에서 관리하고 있다.

준영속 상태에 대해서도 알아보자.

    @Test
    void test() {
        User user = entityManager.find(User.class, 15L);    // 영속
        entityManager.detach(user);    // 준영속
        System.out.println("user = " + user);
    }

 

준영속 상태는 영속성 컨텍스트에 저장되어 있다가 분리된 상태를 말한다. 위와 같이 직접 엔티티 매니저의 detach()를 호출하거나 close()로 영속성 컨텍스트를 닫거나 clear()로 영속성 컨텍스트를 비우면 관리하던 엔티티가 준영속 상태가 된다.

준영속 상태는 객체의 데이터가 지워지는 것이 아니라 영속성 컨텍스트에서 관리되지 않는 것 뿐이기 때문에 위에서 출력에 사용한 것 처럼 엔티티를 계속 사용할 수 있다.

flush()를 호출하는 것은 DB에 쿼리문만 실행하고
여전히 영속성 컨텍스트에서 엔티티를 관리하고 있기 때문에 엔티티가 준영속 상태가 되지는 않는다.

삭제는 remove를 호출하여 할 수 있으며, 영속성 컨텍스트에서 관리되지 않음과 동시에 DB에 delete쿼리를 실행한다.

    @Test
    void test() {
        User user = entityManager.find(User.class, 15L);
        entityManager.remove(user);
        System.out.println("user = " + user.getUsername());
    }

 

삭제 또한 컨텍스트에서 관리되지 않고 DB에 데이터가 없을 뿐 user 엔티티 자체에는 데이터가 있는 것을 확인할 수 있다.

user = maco

 

정리

영속성 컨텍스트의 정의였던 아래의 내용을 살펴 보았다.

1. 각각의 영속적인 엔티티 식별자에 대해 유일한 엔티티 인스턴스가 있는 엔티티 인스턴스의 집합이다.
2. 영속성 컨텍스트 내에서, 엔티티 인스턴스들과 그들의 생명주기가 관리된다.

1차 캐시를 통해 엔티티의 동일성을 보장하고, 성능을 최적화하였고, 엔티티를 영속시켜 엔티티의 생명 주기를 관리했다.

영속성 컨텍스트가 엔티티를 관리하면 다음과 같은 장점들이 있다.

  • 1차 캐시
  • 동일성 보장
  • 트랜잭션을 지원하는 쓰기 지연
  • 변경 감지
  • 지연 로딩

이번 포스팅에서 1차 캐시와, 동일성 보장에 대해서 다뤘으니 다음 포스팅에서 나머지 특징들에 대해서 알아보자.

 

참고 문서

+ Recent posts