영속성 컨텍스트와 1차 캐시

유동우·2025년 2월 8일
0
post-thumbnail

최근 DB 성능 최적화를 위해 레디스를 학습하며 이전에 알고있던 JPA의 1차 캐시와의 차이점이 뭔지 잘 모르겠어서 다시 영속성 컨텍스트의 기본 개념을 학습하고자 한다

JPA (Java Persistence API)

  • JPA는 자바 진영에서 ORM 기술 표준으로 사용되는 인터페이스의 모음이다
  • 풀네임에서 알 수 있듯이 Persistence(영속성)를 기반으로 작동한다
  • 인터페이스 기반이기 때문에 Hibernate, OpenJPA등이 JPA를 구현한다

ORM을 간단히 말하자면 우리가 알고 있는 class와 RDB의 테이블을 매핑해주는것이고, 객체를 관계형 데이터 베이스의 테이블에 자동으로 영속화 해주는 것이다

영속성 컨텍스트

영속성 컨텍스트를 쉽게 한 줄로 나타내면 엔티티를 영구 저장하는 환경이다

엔티티 매니저(EntityManager)를 통해 영속성 컨텍스트에 접근하게 되고, 영속성 컨텍스트는 눈에 보이지 않는 논리적인 개념이다

그렇다면, 영속성 컨텍스트에서 관리하는 엔티티의 생명주기를 알아보자

엔티티 생명주기

생명주기는 비영속, 영속, 준영속, 삭제 상태로 나눌 수 있다
한글말 보다 영어 풀이가 더 와닿을 거라 생각한다

비영속: new/transient

  • 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태이다
// 객체를 생성하고 필드만 설정한 상태 
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

영속: managed

  • 영속성 컨텍스트에서 관리되고 있는 상태
// 객체를 생성하고 필드만 설정한 상태 
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

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

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

준영속: detached

  • 영속성 컨텍스트에 저장되었다가 분리된 상태
// 회원 엔티티를 영속성 컨텍스트에서 분리한 상태
em.detach(member);

삭제: removed

  • 영속성 컨텍스트에서 삭제된 상태
// 객체를 삭제한 상태
em.remove(member)

이들의 관계를 보기쉽게 다이어그램으로 표기하면 다음과 같다

위의 설명과 같이 총 4가지의 상태가 존재하고, 각 메서드를 통해 상태가 변하게 된다
핵심은 DB에서 find() 메서드와 JPQL을 통해 영속성 컨텍스트에서 관리되고 있는 상태의 데이터를 불러올 수 있고, flush() 메서드를 통해 DB에 데이터를 반영하는 것이다

영속성 컨텍스트 안에 1차 캐시SQL 쿼리 저장소가 존재한다.

우선 1차 캐시가 무엇인지 먼저 알아보자.

1차 캐시

1차 캐시는 Map 형식으로 존재한다.

하지만 영속성 컨텍스트의 주기트랜잭션 생명주기와 항상 함께하므로 트랜잭션 종료시 영속성 컨텍스트도 같이 종료된다.

따라서 이때 성능을 더욱 향상시킬 수 있는 기술이 2차캐시 (레디스)이다.

결론은 레디스는 2차 캐시 역할을 하고, 영속성 컨텍스트는 1차 캐시 역할을 한다는 차이점이 존재하는것이다.

그럼 영속성 컨텍스트의 엔티티 영속, 엔티티 조회, 엔티티 수정에 대해 알아보자

엔티티 영속

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

em.persist(member);

엔티티 매니저를 통해 em.persist(member) 실행하게 되면 영속성 컨텍스트에 객체를 등록할 뿐이고, 아직 DB에 INSERT 쿼리를 보내지 않는다.

엔티티 조회

엔티티 매니저를 사용하여 데이터를 조회할때 DB에 접근하기 앞서 우선 1차 캐시에 접근한다

Member findMember = em.find(Member.class, "member1");

현재 영속성 컨텍스트에 member1 이라는 아이디의 member 객체가 존재하므로 1차 캐시 접근만으로 객체를 불러올 수 있다

1차 캐시에 존재하지 않는 데이터를 불러오기 위해서는 DB 접근이 필수적이다

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

우선 조회한 객체에 대해 1차 캐시에 없음을 확인한 후 DB에 접근하여 객체를 가져와 1차 캐시에 저장한 후 반환한다

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");

System.out.println(a == b); //동일성 비교 true

따라서 1차 캐시를 통해 반복 가능한 읽기 등급의 트랜잭션 격리 수준을 애플리케이션에서 제공할 수 있게된다

엔티티 수정

엔티티 매니저는 ❗️영속성 컨텍스트에 등록이 되어있는❗️ 데이터 변경시에 트랜잭션을 시작해야 한다

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

Member memberA = em.find(Member.class, "memberA");

memberA.setUsername("hi");
memberA.setAge(10);

transaction.commit(); // [트랜잭션] 커밋

영속성 컨텍스트에서 관리중인 memberA 아이디에 해당하는 멤버 객체를 불러왔다

이후 usernameage를 수정하고 트랜잭션을 바로 commit한다
commit을 실행하게 되면 비로소 DB에 SQL문을 날리게 된다.

하지만 우린 em.update(member)와 같은 업데이트 코드를 실행하지 않았는데 어떻게 쿼리문이 생기게 될까?

영속성 컨텍스트는 이를 변경 감지(Dirty Checking) 을 통해 해결한다

변경감지란 말 그대로 영속성 컨텍스트에서 관리하는 엔티티의 변경 사항을 감지하는 것이다

영속성 컨텍스트에서 관리중인 데이터의 초기 상태인 스냅샷을 저장한 후, setUsername(), setAge() 같은 메서드를 통하여 변경한 객체의 정보를 파악한다

이 과정에서 UPDATE 쿼리를 생성하여 쓰기 지연 SQL 저장소에 쿼리를 보관하고 해당 쿼리를 commit 을 통해 DB에 날린다
(flush를 통해 직접 쿼리를 날릴 수도 있다)

도식에서 commit 이전에 flush가 발생하는 것을 알 수 있다

flush는 영속성 컨텍스트의 변경 내용을 DB에 반영하는 것이고,
em.flush() 혹은 트랜잭션 커밋과 JPQL 쿼리를 실행하여 자동으로 flush()를 호출할 수 있다

실제 구현된 코드

@Override
public Notification save(Notification notification) {
	return jpaNotificationRepository.save(notification);
}

현재 진행중인 프로젝트에서 사용하는 알림 저장 과정이다
JpaRepository 를 상속받은 JpaNotificationRepository를 통해 save() 메서드를 실행하였다

@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S entity);
}

save() 메서드는 CrudRepository 에 존재하고,
그림과 같이 JpaRepository() <- ListCrudRepository() <- CrudRepository() 를 상속하게 된다

CrudRepository에 정의된 save() 메서드는 SimpleJpaRepository 에서 구현한다

@Transactional
public <S extends T> S save(S entity) {
	Assert.notNull(entity, "Entity must not be null");
	if (this.entityInformation.isNew(entity)) {
		this.entityManager.persist(entity);
		return entity;
	} else {
            return (S)this.entityManager.merge(entity);
    }
}

if (this.entityInformation.isNew(entity))
해당 메서드에서 알 수 있듯

영속성 컨텍스트에 존재하지 않는 새로운 엔티티면
entityManger.persist(entity)를 통해 해당 엔티티를 영속 상태로 만들어 영속성 컨텍스트에서 관리할 수 있게한다

영속성 컨텍스트에 이미 존재하는 엔티티면
entityManager.merge(entity) 를 수행하게 되는데...

트랜잭션 커밋

언제 트랜잭션을 실행 및 종료하여 SQL 쓰기 지연 저장소에서 쿼리를 DB로 날려서 반영하게 될까?

@Transactional
public Product createProduct(String name) {
	Product product = new Product();
    product.setName(name);

	// 아직 DB에는 반영되지 않음 (영속성 컨텍스트에 저장됨)
	productRepository.save(product);

	// 메서드가 정상적으로 종료되면 커밋됨 -> 그때 DB에 반영됨
	return product;
 }

@Transactional 어노테이션이 붙은 메서드가 종료되면 자동으로 커밋을 발생시키고, 이때 예외가 발생하면 persist는 수행된 채로 롤백이 이루어진다.

profile
효율적이고 꾸준하게

0개의 댓글

관련 채용 정보