[JPA]영속성 컨텍스트

주운·2024년 2월 7일
post-thumbnail

1. 엔터티 매니저

JPA의 기능은 크게 두 부분으로 나눌 수 있다.
엔터티와 테이블을 매핑하는 설계와 매핑한 엔터티의 실제 사용이다.

엔터티의 사용은 엔터티 매니저를 통해 이루어진다.

엔터티 매니저는 엔터티의 저장, 수정, 삭제, 조회 등 엔터티와 관련된 모든 일을 처리한다.

엔터티 매니저 팩토리와 엔터티 매니저

한 개의 데이터베이스를 사용하는 어플리케이션은 일반적으로 하나의 엔터티 매니저 팩토리를 공유한다.

하나의 팩토리를 생성한 후 필요할 때마다 엔터티 매니저를 생성해 사용한다.

엔터니 팩토리는 생성 비용이 상당히 크기 때문에 하나만 만들어 어플리케이션 전체에서 공유한다.

엔터티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하지만, 엔터티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하기 때문에 스레드 간에 절대 공유하면 안 된다.

위 그림을 보면 하나의 팩토리에서 여러 개의 매니저를 생성한다.
엔터티 매니저는 연결이 필요한 시점까지 커넥션을 얻지 않고 기다렸다가 트랜잭션을 시작할 때 커넥션을 획득한다.

스프링 부트의 엔터티 매니저 팩토리

스프링 부트는 내부에서 엔터티 매니저 팩토리를 하나만 생성해 관리한다.
사용자는 @PersistenceContext 혹은 @Autowired 애너테이션을 이용해서 엔터티 매니저를 사용한다.

스프링 부트는 기본적으로 하나의 빈만 생성해 공유하기 때문에 동시성 문제가 발생할 수 있다.
그렇기에 실제 엔터티 매니저가 아닌 프록시(가짜) 엔터티 매니저를 사용하고, 필요할 때 실제 엔터티 매니저를 호출한다.

한마디로, 스프링 부트에서는 사용자가 직접 엔터티 매니저 팩토리를 사용하지 않는다.

커넥션 풀

스프링과 같은 J2EE환경은 커넥션풀을 제공하며 JPA 사용시 컨테이너가 제공하는 데이터소스(데이터베이스 커넥션 정보)을 사용하게 된다.

데이터 커넥션의 획득을 위해서는 다음의 과정을 거쳐야 한다.

  1. 커넥션 조회
  2. 서버와 DB 사이 TCP/IP 커넥션 생성
  3. 인증, DB 세션 생성
  4. 커넥션 생성 완료 응답
  5. 클라이언트에 커넥션 객체 반환

데이터베이스에 접근할 떄마다 이러한 과정을 거치면 응답시간이 증가하게 된다.

커넥션 풀은 이러한 문제를 해결하기 위해 커넥션을 미리 생성해두고 관리하는 방법이다.

2. 영속성 컨텍스트

영속성 컨텍스트(Persistence context)는 JPA를 이해하는데 가장 중요한 특성이다.
영속성 컨텍스트는 엔터티를 영구 저장하는 환경이다.

영속석 컨텍스트는 엔터티 매니저가 생성될 때 하나 만들어지며 엔터티 매니저는 영속성 컨텍스트에 엔터티를 저장하고 관리한다.

3. 영속성 컨텍스트 특징

  • 영속성 컨텍스트와 식별자 값
  • 영속성 컨텍스트는 엔터티를 식별자 값(@Id 어노테이션으로 테이블 기본키와 매핑한 값)으로 구분한다.
    따라서 영속성 컨텍스트와 관계된 엔터티는 반드시 식별자 값이 있어야 한다.
  • 영속성 컨텍스트와 데이터베이스 저장
  • 영속성 컨텍스트에 저장된 엔터티는 트랜잭션을 커밋하는 순간 데이터베이스에 반영된다. 이를 flush라고 한다.

영속성 컨텍스트를 이용하면 다음과 같은 장점이 있다.

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

1차 캐시

영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이를 1차 캐시라고 한다.
캐시에 저장된 엔터티들은 @Id로 매핑된 식별자로 구분된다.

em.persist()로 엔터티를 저장하면 엔터티가 영속성 컨텍스트 내부의 1차 캐시에 저장된다.

엔터티 매니저는 엔터티를 조회할 때 먼저 1차 캐시에서 데이터를 조회한다.
1차 캐시에서 데이터를 찾지 못하면 데이터베이스에서 조회한다.

1차 캐시는 데이터베이스 접근 횟수를 줄여 데이터 조회를 빠르게 한다.

동일성 보장

같은 값을 가진 객체라 해도 다른 주소값을 가진다면 다른 인스턴스로 인식된다.
그렇기에 ==와 같이 동일성을 확인하는 방법으로는 false의 결과가 나오게 된다.

하지만 영속성 컨텍스트는 같은 Id를 가지는 인스턴스에 대해 동일성을 보장한다.

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

System.out.println(a==b);

위와 같이 member1을 조회할 때 1차 캐시에 데이터가 없다면 데이터베이스에서 member1을 조회하여 1차 캐시에 저장한다.
1차 캐시에 저장된 엔터티를 반환한다.

두 번째 조회에서는 데이터베이스에 연결하지 않고 1차 캐시에서 바로 엔터티를 받을 수 있다.

항상 1차 캐시 내부의 같은 인스턴스를 반환받기 때문에 인스턴스의 동일성이 보장된다.

쓰기 지연

트랜잭션을 커밋하기 전에는 데이터베이스에 등록, 수정, 삭제 쿼리를 보내지 않고 모았다가 커밋 시에 한꺼번에 데이터베이스에 반영한다.

데이터 저장시마다 쿼리를 보내더라도 실제 적용은 트랜잭션 단위로 된다.
트랜잭션이 롤백되면 적용되지 않는 것은 동일하기 때문에 쓰기 지연으로 인해 결과가 달라지지는 않는다.

쓰기 지연을 통해 쿼리를 한 번에 전달해 성능을 최적화할 수 있다.

변경 감지

JPA에서 엔터티를 변경하려면 단순히 엔터티 객체의 데이터만 변경하면 된다.

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

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

일반 SQL문을 사용할 때 수정이 필요한 필드에 따라 여러 쿼리를 사용해야 하는 것과 비교하면 훨씬 간단하다.

jpa는 스냅샷을 이용해 엔터티의 변경을 감지한다.
엔터티를 영속성 컨텍스트에 보관할 때 최초 상태를 복사해 저장하는데 이를 스냅샷이라 한다.

  1. 위와 같이 엔터티를 변경 후 트랜잭션을 커밋하면 flush가 호출된다.

  2. 엔터티와 스냅샷을 비교하여 변경된 부분이 있는지 확인한다.

  3. 변경된 엔터티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.

  4. SQL이 데이터베이스에 보내진다.

  5. 데이터베이스 트랜잭션이 처리된다.

JPA는 엔터티를 업데이트할 때 수정한 필드와 상관없이 항상 전체 필드를 업데이트하는 쿼리를 보낸다.
이는 아래의 장점을 가진다.

  • 항상 같은 수정 쿼리를 사용한다.
  • 이전에 사용한 쿼리를 재사용할 수 있다.

필드가 많거나 저장되는 데이터가 너무 크면 해당 부분만 동적으로 UPDATE SQL을 생성해 처리할 수 있다.

지연 로딩

쿼리로 데이터를 조회할 때 연관된 데이터를 바로 로딩하지 않고, 실제로 사용될 때 쿼리를 날려 데이터를 조회한다.

public class Product {

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    @Column(name = "product_id")
    private int id;
  
    @ManyToOne
    @JoinColumn(name = "userId")
    private User user;
    
    @ManyToOne
    @JoinColumn(name = "categoryId")
    private Category category;

위와 같이 N:1 관계로 매핑된 엔터티는 조회시에 연관된 데이터까지 조회한다.

즉시 로딩을 사용하면 쿼리를 3개 날려 user, category 데이터까지 가져오지만,

지연 로딩을 사용하면 실제로 user, category 데이터를 필요로 할 때 추가적으로 쿼리를 날려 데이터를 가져온다.

https://goldenrabbit.co.kr/2023/06/08/springjpa/
https://thalals.tistory.com/368
자바 ORM 표준 JPA 프로그래밍

0개의 댓글