JPA 영속성 컨텍스트, 캐시, 쓰기 지연

신지훈·2025년 1월 22일
0

JAVA/Spring

목록 보기
1/6
post-thumbnail

1. JPA란?

jpa란 자바 어플리케이션에서 관계형 데이터베이스 사용하는 방식을 정의한 인터페이스이다. Spring에서는 Spring Data JPA라는 라이브러리를 제공해 개발자가 JPA를 더 쉽게 사용할 수 있도록 해준다.
이 라이브러리에서 JPA를 한단계 추상화 시킨 Repository인터페이스를 제공함으로써 build추가 후 아래와 같이 인터페이스만 정의해주면 JPA의 CRUD를 바로 사용할 수 있다.
하지만 이런 기능을 영속성 컨텍스트와 관련된 개념을 잘 이해하지 못한다면 데이터 손실이 일어날 수 있다.

public interface StudentRepository extends JpaRepository<Student, Long> {}

2. @Transactional

영속성 컨텍스트에 대해 알아보기 전에 Transactional에 대해 알아보자. JPA의 모든 데이터 변경은 트랜잭션이라는 범위 안에서 일어난다.
트랜잭션 밖에서의 데이터 변경은 반영되지 않고 Spring Data JPA에서 변경이 일어나는 코드는 @Transactional이라는 어노테이션이 추가되어있다. 예를 들어 사진과 같이 save()등의 crud에도 @Transactional이 붙어있는 걸 볼 수 있다.

3. 영속성 컨텍스트

영속성 컨텍스트는 entity를 저장하고 관리하는 저장소이며, 어플리케이션과 데이터베이스 사이에 entity를 보관하는 가상의 데이터베이스 같은 역할을 한다. 위에 save() 메소드에서 보면 em.persist()와 em.merge() 메소드를 볼 수 있다. em.persist()를 통해 entity를 영속성 컨텍스트에 저장하고 이미 entity가 영속상태일 경우 em.merge()를 통해 덮어쓴다.

영속성 컨텍스트가 어플리케이션과 Database사이에 있으면서 다음과 같은 기능을 제공한다. 다음의 기능에 대해 자세히 살펴보자.
1,2차캐시
쓰기 지연 SQL저장소
**Dirty Checking

4. 1,2차 캐시

1차 캐시
영속성 컨텍스트 안에는 엔티티를 보관하는 저장소인 1차 캐시를 가지고 있다.
em.persist를 하는 순간 pk값(id)과 객체를 맵핑하여 1차 캐시에 가지고 있어 같은 엔티티가 있으면 객체 동일성을 보장한다. 또 위에서 언급했듯이 이 1차캐시는 트랜잭션이 시작하고 종료할 때까지만 유효하다.

이렇게 1차캐시를 사용하는 이유는 복잡한 로직을 가진 경우 객체를 여러번 조회해야하는 경우가 있다. 하지만 매번 데이터베이스에 직접가서 조회하는 것은 매우 비 효율적이므로 처음 조회할 때 엔티티를 1차 캐시에 저장해두고 다음에 조회할 때는 1차캐시에 저장되어 있는 객체를 조회해서 사용함으로서 성능을 개선할 수 있다.

다시 한 번 강조하지만 1차 캐시를 한 트랜잭션 안에서만 공유하기에 한 트랜잭션 내에서 여러번 조회할 때 성능상의 이점이 있다.

그렇다면 전체 어플케이션이 공유하는 캐시는 없을까? 그것이 바로 2차 캐시이다.

2차 캐시
1차 캐시는 영속성 컨텍스트안에 내부에 존재하는 엔티티 보관소였다면 2차 캐시는 애플리케이션 범위에서 공유하는 캐시공유 캐시라고도 불린다.

JPA에서 데이터를 조회할 때 먼저 1차캐시에 조회, 2차 캐시 조회, DB순으로 조회를 한다.

DB에서 조회한 데이터는 2차 캐시에 보관되고, 2차 캐시는 해당 엔티티의 복사본을 만들어 1차 캐시에 반환하며, 이는 동시성을 극대화시키기 위함이다.

예를 들어, 2차 캐시에 저장된 엔티티를 여러 곳에서 동시에 수정하면 문제가 발생하기 때문에 락을 걸어야 하는데, 이는 동시성의 저하를 일으키기 때문에, 복사본을 반환하여 동시에 수정할 수 있도록 한다.

하지만 데이터를 저장할 때는 1차 캐시에 저장한 후 2차캐시를 거치지 않고 flush하여 db에 직접 반영한다.

그렇다면 2차 캐시의 내용과 db의 내용이 달라지는 것이 아닌가?? 라는 생각이 들어 조사해보니 2가지 방법을 통해 일관성을 유지하는 것 같다.

  1. 캐시 동시성 전략
  • READ_ONLY: 읽기 전용 데이터에 사용
  • READ_WRITE: 읽기/쓰기가 모두 필요한 경우
  • NONSTRICT_READ_WRITE: 데이터 수정이 거의 없는 경우
  1. 캐시 무효화 정책 :
  • TTL(Time-to-Live)설정으로 일정 시간 후 캐시 자동 갱신
  • 데이터 변경 시 캐시 자동 무효화

+자세한 내용은 공부 필요

5. 쓰기 지연

객체를 persist를 하는 순간 pk값과 객체가 맵핑되어 1차 캐시에 저장된다고 하였다. 이때 1차캐시에 저장하면서 아래 그림과 같이 쓰기 지연 SQL 저장소에 쿼리를 만들어 쌓는다. 그리고 이 객체에 대한 SQL문을 저장해 두었다가 트랜잭션이 종료되거나 EntityTransaction.commit()이 명시적으로 호출될 때 commit하고, 그 때 flush되면서 DB에 SQL문이 반영이 된다. flush란 영속성 컨텍스트의 변경 내용을 DB에 반영하며, 1차 캐시를 지우지는 않는다

6. Dirty Checking

여기서 Dirty란 상태의 변화가 생긴 정도를 의미한다. JPA에서 트랜잭션이 끝나는 시점에 변화가있는 모든 entity 객체를 데이터 베이스에 자동으로 반영해 준다. 이것을 이해가기 위해서 먼저 스냅샷에 대해 알 필요가 있다.

앞서 언급하지 않았지만 영속성 컨텍스트에 1차 캐시가 저장되었는데 이때 pk값, 객체와 함께 스냅샷이 함께 맵핑되어 저장된다. 여기서 스냅샷에는 객체의 처음 상태 값이 저장되어 있다.

그래서 트랜잭션이 끝나는 시점에 스냅샷과 객체의 변화를 비교하여 쓰기지연 SQL 저장소에 자동으로 변화한 내용을 SQL문을 만들어 저장한다. 그리고 Commit되서 Flush가 되면 해당 SQL문들이 모두 반영되게 된다.

이런 경우 조회한 객체를 변경후 따로 저장하지 않아도 자동으로 db에 반영되게 된다.

이것과 관련된 옵션이 @Transactional(readOnly = true)이다. 이 경우 스냅샷, dirty checking 하지 않음을 의미한다. 당연한 이야기지만 Dirty Checking은 영속성 컨텍스트가 관리하는 entity에만 적용된다.

profile
주주주주니어 개발자

0개의 댓글