velog에서 이전한 글 입니다.

JPA

Persistence Context


영속성 컨텍스트 : Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간이다. 객체의 생명(유지되는 시간)이나 공간(위치)을 유지하고 이동한다.

 

영속성 컨텍스트에 접근하고 조작하기 위해선 EntityManager가 필요하고 EntityManagerFactory를 통해 생성하여 사용할 수 있다.


EntityManagerFactory는 일반적으로 DB 하나에 하나만 생성되어 애플리케이션이 동작하는 동안 사용된다.
(Spring boot jpa를 사용하기 전에는 /resources/META-INF/ 위치에 persistence.xml 파일을 만들어 정보를 넣어둔다.)

EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
EntityManager em = emf.createEntityManager();

트랜잭션

트랜잭션은 DB 데이터들의 무결성(integrity)과 정합성(consistency)을 유지하기 위한 하나의 논리적 개념으로 DB의 상태를 변화시키기 위한 작업 수행의 논리적 단위이다.
무결성 : 데이터 값이 정확한 상태
정합성 : 어떤 데이터들의 값이 서로 일치함

START TRANSACTION; # 트랜잭션을 시작

INSERT INTO memo (id, username, contents) VALUES (1, 'Robbie', 'Robbie Memo');
INSERT INTO memo (id, username, contents) VALUES (2, 'Robbert', 'Robbert Memo');
SELECT * FROM memo;

COMMIT; # 트랜잭션 커밋
SELECT * FROM memo;

DB에서 하나의 트랜잭션에 여러 개의 SQL을 포함하고 있다가 마지막에 영구적으로 변경을 반영하는 것 처럼 JPA에서도 영속성 컨텍스트로 관리하고 있는 변경이 발생한 객체들의 정보를 쓰기 지연 저장소에 전부 가지고 있다가 마지막에 SQL을 한번에 DB에 요청해 변경을 반영한다.

EntityTransaction et = em.getTransaction(); // EntityManager 에서 EntityTransaction 을 가져오기.
et.begin(); // 트랜잭션을 시작.
try {
    ...
    em.persist(entity);
    et.commit(); //정상적으로 수행되었다면 db에 반영
} catch (Exception ex) {
    et.rollback(); //실패시 rollback
} finally {
    em.close(); //사용한 manager 종료
}
emf.close(); //사용한 factory 종료

트랜잭션 sql 중 단 하나라도 실패한다면 모든 변경을 되돌린다.(정합성)
em.commit()em.flush()가 자동 호출된다. 쓰기 지연 저장소의 SQL들을 DB에 요청하는 역할은 flush가 수행한다.

Persistence Context 기능

영속성 컨텍스트는 내부적으로 1차 캐시를 가지고 있다. Map자료구조 형태로 되어있으며 key에는 기본 키 즉, 식별자 값을 저장하고 value에는 해당 entity 객체를 저장한다.

Entity 조회

캐시 저장소에 조회하는 id가 존재하지 않다면 db에 select 조회 후 캐싱하고 값이 있다면 바로 캐시의 entity 객체를 반환한다.

 

1차 캐시를 사용함으로 쿼리 횟수를 줄이고 객체 동일성을 보장할 수 있다.

        Memo memo1 = em.find(Memo.class, 1);
        Memo memo2 = em.find(Memo.class, 1);
        System.out.println(memo1 == memo2); //true

객체 동일성은 같은 객체 주소를 참조하고 있음을 말한다.

Entity 삭제


삭제의 경우 조회와 동일하게 1차 캐시에 없는 객체라면 select해오고 entity를 deleted상태로 만든다. 후에 commit시점에 delete sql로 동작한다.

Entity 수정
영속성 컨텍스트에 저장된 Entity가 변경될 때마다 Update SQL이 쓰기 지연 저장소에 저장된다면? 하나의 sql로 가능한 상황을 여러번 쿼리를 요청하기에 비효율적이다.


jpa는 위와 같은 Update문제를 최초 상태(LoadedState)로 해결하고 그 과정을 변경 감지, Dirty Checking이라 부른다.
em.flush() 가 호출되면 Entity의 현재 상태와 최초 상태를 비교하고 변경 내용이 있다면 Update sql을 생성하여 쓰기 지연 저장소에 저장한 후 모든 sql을 DB에 요청한다.

Entity의 상태

  • 비영속(Transient) : 영속성 컨텍스트의 관리를 받지 않는 상태이다.
    Memo memo = new Memo();
  • 영속(Managed) : 영속성 컨텍스트에서 관리되는 상태이다.
    em.persist(entity)
  • 준영속(Detached) : 관리되다가 분리된 상태이다.
    em.detach(entity)
  • 삭제(Removed) : DELETED 상태이다.
    em.remove(entity)

em.merge(entity) : 준영속을 다시 영속으로 바꿀 때 사용한다.
해당 entity가 영속성 컨텍스트에 없다면 db에서 조회하고 인자로 받은 entity 값으로 update sql을 수행한다. db에 없다면 새롭게 생성한 entity를 context에 저장하고 insert sql이 수행된다.

 

em.persist()em.merge()는 어떤 차이가 있을까?

  Memo mergedMemo = em.merge(memo);
  System.out.println(em.contains(memo)); //false
  System.out.println(em.contains(mergedMemo)); //true

준영속 상태의 memo는 merge()호출 후에도 영속성 컨텍스트에 저장되어 있지 않고 return entity는 저장되어 있다.

동작에는 차이가 없을까?

            Memo memo = new Memo();
            memo.setId(2L);
            memo.setContents("11");
            memo.setUsername("choi22");
            em.persist(memo);

persist는 캐시에 값이 없을 때 db를 조회하지 않고 그냥 insert로 들어간다. 그래서 식별자2 row가 db에 들어가 있다면 위 코드는 sql에러를 뱉는다.
merge를 쓴다면 db를 조회하여 불러오고 변경이 있다면 update로 동작한다.

이어서

spring boot에선 jpa를 어떻게 다루는지 살펴보자.

2023.07.13 - [spring] - 스프링) Spring Boot의 JPA @Transactional/SimpleJpaRepository (23-07-09)