3. 영속성 관리

xellos·2022년 6월 16일
0

JPA

목록 보기
3/7

JPA 가 제공하는 기능은 크게 엔티티와 테이블을 매핑하는 설계 부분과 매핑한 엔티티를 실제 사용하는 부분으로 나눌 수 있다. 여기에서는 매핑한 엔티티를 엔티티 매니저를 통해 어떻게 사용하는지 알아보자.

엔티티 매니저는 엔티티를 저장, 수정, 삭제, 조회하는 등의 엔티티와 관련된 모든 일을 처리한다. 이름 그대로 엔티티를 관리하는 관리자다. 개발자 입장에서 엔티티 매니저는 가상의 DB로 생각하면된다.

엔티티 매니저 팩토리는 이름 그대로 엔티티를 만드는 공장인데 이를 만드는 비용이 매무 크므로, 한 개만 만들어서 애플리케이션 전체에 공유하도록 설계되어 있다 (엔티티 매니저를 생성하는 비용을 거의 들지 않음). 엔티티 매니저 팩토리는 여러 쓰레드가 동시에 접근해도 안전하므로 서로 다른 쓰레드간에 공유해도 되지만, 엔티티 매니저는 여러 쓰레드가 동시에 접근하면 동시선 문제가 발생하므로 쓰레드간 절대로 공유하면 안된다.

위의 그림에서 보듯 엔티티 매니저는 팬토리에서 생성되며 실제 DB 접근이 필요하기 전까지는 커넥션 풀을 사용하지 않고 대기한다.


영속성 컨텍스트란?

JPA를 이해하는데 가장 중요한 개념은 영속성 컨텍스트(Persistence Context)다.

이는 엔티티를 영구저장하는 환경으로 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관 ・ 관리 한다.

  • 아래 코드는 단순히 엔티티를 저장하는 것이 아니라, 정확히 말하면 persist() 메서드는 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장한다.
em.persist(member);

1) 엔티티 생명주기

엔티티는 다음과 같은 4가지 상태가 존재한다.

  • 비영속(new): 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속(managed): 영속성 컨텍스트에 저장된 상태 (영속성 컨텍스트가 관리하는 엔티티 → 영속 상태: em.persist)
  • 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태(em.detach, em.close, em.clear)
  • 삭제(remove): 삭제된 상태(em.remove)

2) 영속성 컨텍스트의 특징

영속성 컨텍스트와 식별자 값

영속성 컨텍스트는 엔티티를 식별자 값@Id로 구분한다. 따라서 영속 상태는 식별자 값이 반드시 있어야 한다.

영속성 컨텍스트와 DB 저장

JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 DB에 저장하는데 이를 플러시라고 한다.

영속성 컨텍스트가 엔티티를 관리시 장점

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

엔티티와 영속성 컨텍스트

엔티티 저장, 조회, 삭제 등과 같은 상황에서 엔티티와 영속성 컨텍스트가 어떠한 관계를 가지는지 실펴보자

1) 엔티티 조회

영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이를 1차 캐시라고 한다. 영속상태의 엔티티는 모두 이곳에 저장된다.

  • 영속 상태 == 1차 캐시 저장 상태

1차 캐시의 키는 식별자값 @Id이다. 그리고 식별자 값은 DB의 기본 키와 매핑되어있다. 따라서 영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 DB의 기본키 값이다.

em.persist(member);

장점

em.find()로 데이터 조회시 1차 캐시에서 식별자 값으로 엔티티를 조회한다.

em.persist(member);

//1차 캐시에서 조회
Member findOne = em.find(Member.class, "member1");

데이터베이스에서 조회

만약 em.find()를 호출시 엔티티가 1차 캐시에 없으면 엔티티 매니저는 DB를 호출해서 엔티티를 생성한다. 그리고 1차 캐시에 저장한 후 영속 상태의 엔티티를 반환한다.

영속 엔티티의 동일성 보장

이미 영속성 컨텍스트에 존재하는 엔티티를 반복해서 호출시 저장되어 있는 같은 인스턴스를 반환한다. 따라서 아래의 코드는 참이 된다. 결과적으로 영속성 컨텍스트는 성능상 이점엔티티의 동일성 을 보장한다.

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

m1 == m2 //true

2) 엔티티 저장

영속성 컨텍스트는 트랜잭션이 커밋하기 전까지는 추가사항이나 변경사항을 내부에 가지고 있다. 이후 트랜잭션이 커밋될때 모든 요청사항을 DB에 보낸다. 이러한 동작 방식은 매 요청마다 DB에 접근하지 않기 때문에 성능상의 이점이 존재한다.

  • 커밋전: 요청 사항을 영속성 컨텍스트에 보관하고 미리 SQL을 생성한다.
  • 커밋 발생시: flush()가 발생하여 변경사항을 DB에 전달하고 커밋한다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();

//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin();	//[트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);

//여기까지 INSERT SQL을 DB에 보내지 않는다.
//커밋하는 순간 DB에 SQL을 보낸다.
trasaction.commit();	//[트랜잭션] 커밋

3) 엔티티 수정

기존의 방식은 엔티티가 변경될 때마다 수정과 관련된 SQL이 변경되어야 한다는 문제가 있다. 예를 들어 엔티티 필드가 하나 늘거나 줄어들면 이를 반영하는 UPDATE문을 작성해야 한다. 이러한 문제는 수정 쿼리가 많아지는 것은 물론이고 비즈니스 로직을 분석하기 위해 SQL을 계속 확인해야 한다. 결국 직접적이든 간접적이든 비즈니스 로직이 SQL에 의존하게 된다.

변경감지

JPA에서는 단순히 엔티티를 조회해서 데이터만 변경하면 된다. (이때, 반드시 엔티티는 영속상태) 이렇게 하면 영속성 컨테스트 내부에서 저장된 최초의 스냅샷과 비교하여 변경사항을 발견하고 자동으로 SQL을 생성하고 커밋시 flush로 변경사항을 DB에 반영한다.

특징

  1. 변경감지시 JPA 기본 전략으로 엔티티의 모든 필드를 업데이트한다.
  2. 그로인하여 전송량이 증가한다.(단점)
  3. 그러나 수정 쿼리가 항상 동일하며, DB는 이전에 한 반 파싱된 쿼리를 재사용할 수 있다.(장점)

플러시

플러시는 영속성 컨텍스트의 변경 내용을 DB에 반영한다(동기화). 플러시를 실행하면 구체적으로 다음과 같은 일이 일어난다.
1. 변경감지가 동작해서 영속성 컨텍스트의 모든 엔티티를 스냅샷과 비교하여 수정된 엔티티를 찾고, 수정된 엔티티는 쿼리를 만들어 쓰기 지연 SQL 저장소에 저장한다.
2. 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송한다.

1) 플러시 하는 방법

  1. em.flush()
  2. 트랜잭션 커밋시
  3. JPQL 쿼리 실행시 → 이전에 영속성 컨텍스트에만 있는 변경 상태를 반영하고 JPQL을 수행하기 위함
  • 옵션
FlushModeType.AUTO: 커밋이나 쿼리 실행시 플러시(DEFAULT)
FlushModeType.COMMIT: 커밋할 때만 플러시 

em.setFlushMode(FlushModeType.COMMIT); //직접 설정

준영속

영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것을 준영속 상태라고 한다. 따라서 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.

영속 상태의 엔티티를 준영속 상태로 만드는 방법은 크게 3가지다.
1. em.detach(entity) : 특정 엔티티만 준영속 상태로 전환한다.
2. em.clear() : 영속성 컨텍스트를 완전히 초기화한다.
3. em.close() : 영속성 컨텍스트를 종료한다.

엔티티가 준영속 상태가 되는 순간 1차 캐시부터 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거된다.

1) 준영속 상태의 특징

  • 거의 비영속상태에 가깝다.
  • 식별자 값을 가지고 있다.
  • 지연로딩을 할 수 없다.

2) 병합: merge

준영속 상태의 엔티티를 다시 영속 상태로 변경하려면 병합을 사용하면 된다. merge() 메서드는 준영속 상태의 엔티티를 받아서 그 정보로 새로운 상태의 영속 엔티티를 반환한다.

Member mergeMember = em.merge(member);

위의 과정은 다음과 같다.
1. merge()를 실행한다.
2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다. 만약 1차 캐시에 엔티티가 없으면 DB에서 엔티티를 조회하고 1차 캐시에 저장한다.
3. 조회한 영속성 엔티티에 member 엔티티 갑을 채워 넣는다 (값이 수정된다).
4. mergeMember를 반환한다.

이때, 준영속 상태인 member 엔티티와 영속 상태 mergeMember 엔티티는 서로 다른 인스턴스다.

비영속 병합

병합은 비영속 엔티티도 영속상태로 만들 수 있다. 벙합은 파라미터로 넘어온 엔티티의 식별자 값으로 영속성 컨텍스트를 조회하고 찾는 엔티티가 없으면 DB에서 조회한다. 만약 없으면 새로운 엔티티를 생성해서 병합한다.
→ 식별자 값으로 엔티티를 조회할 수 있으면 불러서 병합하고 없으면 생성해서 병합한다.

0개의 댓글