이전 글에서 JPA에 대한 기본 정리를 진행해보았습니다.
이번 글에서는 JPA 에서 가장 중요할 수 있는 영속성 컨텍스트(Persistence Context)에 대해서 정리해보고자 합니다.
1. 영속성 컨텍스트란?
- 엔티티(Entity)를 영구 저장하는 환경.
- 어플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 저장소 같은 역할.
- 엔티티 매니저(EntityManager)를 통해 영속성 컨텍스트에 접근.
- EntityManager 를 통해 Entity 를 저장, 조회하면 EntityManager 는 영속성 컨텍스트에 해당 Entity 를 보관하고 관리.
2. 영속성 컨텍스트 생명주기
- 영속성 컨텍스트의 생명주기는 아래 이미지와 같이 4가지 상태가 있다.
- 비영속(new) : 영속성 컨텍스트와는 무관한 상태
- 영속(managed) : 영속성 컨텍스트에 저장된 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed) : 영속성 컨텍스트에서 삭제된 상태
2-1. 비영속(New)
- Entity 객체를 생성했지만, 아직 영속성 컨텍스트에는 저장되지 않은 상태.(순수 객체)
Member member = new Member();
2-2. 영속(Managed)
- Entity Manager를 통해서 Entity를 영속성 컨텍스트에 저장한 상태.
- 해당 객체는 영속성 컨텍스트에 의해 관리된다는 의미.
// 순수 객체
Member member = new Member();
// 영속성 컨텍스트에 저장 -> 영속성 컨텍스트가 관리
EntityManager em;
em.persist(member);
2-3. 준영속(Detached)
- 영속성 컨텍스트가 관리하던 상태에서 엔티티를 더이상 관리하지 않는 상태.
- 준영속 상태의 특징
- 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않는다.
- 식별자 값을 가지고 있다.
// 엔티티를 영속성 컨텍스트에서 분리.
em.detach(member);
// 영속성 콘텍스트를 비움(초기화).
em.claer();
// 영속성 콘텍스트를 종료.
em.close();
- 엔티티를 준영속 상태로 전환하는 방법
- detach(entity)
- 특정 엔티티만 준영속 상태로 전환(영속성 컨텍스트로부터 분리)
- 1차 캐시, 쓰기 지연, SQL 저장소 정보 제거
- 영속성 컨텍스트 안에서의 Insert, Update 쿼리도 제거되어 DB에 저장되지 않음 - clear()
- 영속성 컨텍스트를 완전히 초기화
- 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만듦
- 영속성 컨텍스트 틀은 유지하지만 내용은 비워 새로 만든 것과 같은 상태 - close()
- 영속성 컨텍스트를 종료
- 해당 영속성 컨텍스트가 관리하던 영속성 상태의 엔티티들은 모두 준영속 상태로 변경
- 엔티티를 영속 상태로 전환하는 방법
- merge(entity)
- 준영속 상태의 엔티티를 다시 영속 상태로 변경(병합)
- 파라미터로 전달된 엔티티의 식별자 값으로 영속성 컨텍스트를 조회하고 엔티티가 없다면, DB에서 조회
- 만약 DB에서도 없다면 새로운 엔티티를 생성하여 병합
- 병합은 save(저장) 또는 update(수정) 기능 수행
2-4. 삭제(Remove)
- 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제.
em.remove(member);
3. 특징
- 엔티티 매니저(Entity Manager)를 생성할 때 영속성 컨텍스트(Persistence Context)도 생성(1:1)
- 엔티티 매니저를 통해 해당 영속성 컨텍스트에 접근, 관리 할 수 있음
- 엔티티를 식별자 값(@id로 매핑한 값)으로 구분
- 영속성 상태에는 식별자 값이 반드시 있어야 한다.(없으면 예외 발생)
- 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티(Entity)를 DB에 반영
- 영속성 상태에서 값을 여러 번 바꾸어도 마지막 트랜잭션을 커밋하는 순간 값으로 반영
- 플러시(flush)
- 영속성 상태의 엔티티는 모두 영속성 컨텍스트에서 관리
- 내부 캐시(1차 캐시)에 저장
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
- 지연 로딩
- 아래와 같이 간단한 Member 엔티티를 예로 들어보겠습니다.
@Entity
@Getter
@Setter
public class Member {
@Id
private String id;
private String name;
private String phoneNumber;
}
// 엔티티 생성(비영속=순수객체)
Member member = new Member();
member.setId("member1");
member.setName("개발자");
member.setPhoneNumber("01012345678");
Member member2 = new Member();
member2.setId("member2");
member2.setName("이순신");
member2.setPhoneNumber("01099995555");
// 엔티티 영속
EntityManager em;
em.persist(member); // id = member1
em.persist(member2); // id = member2
3-0. 1차 캐시
- 영속성 컨텍스트는 내부에 캐시(Cache)가 존재(1차 캐시)
- 영속 상태의 엔티티는 해당 캐시에 저장
- 캐시는 Map 형태로 구성되어 있으며, 키(Key)는 @Id로 매핑한 식별자이며 값(Value)은 엔티티 인스턴스
- 1차 캐시의 키는 식별자 값
- 식별자 값은 DB(데이터베이스) 기본키와 매핑
- 영속성 컨텍스트에서 데이터를 저장하고 조회하는 모든 기준은 DB 기본 키 값
3-1. 엔티티 조회
Member member = em.find(Member.class, "member1"); // id = member1
- 1차 캐시에서 해당 Key에 대한 엔티티가 있는지 조회
- 엔티티 조회 시 우선 1차 캐시에서 식별자 값으로 엔티티를 찾음.
- 엔티티가 존재하면 DB 조회를 하지 않고, 메모리에 있는 1차 캐시에서 해당 엔티티를 조회.
- 엔티티가 1차 캐시에 없다면, DB에서 조회
- 엔티티가 1차 캐시에 없으면 엔티티 매니저는 DB 를 조회해서 해당 엔티티를 1차 캐시에 저장.
- 1차 캐시에 저장된 후 영속성 상태의 엔티티를 반환.
- 영속성 엔티티의 동일성 보장
Member m1 = em.find(Member.class, "member1");
Member m2 = em.find(Member.class, "member1");
System.out.println(a == b) // true (동일성 보장)
3-2. 엔티티 등록
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 데이터 변경 시 트랜잭션을 시작.
transaction.begin();
em.persist(member1);
em.persist(member2);
// 여기까지는 INSERT 쿼리를 DB에 보내지 않음.
// 커밋하는 순간 DB에 INSERT 쿼리를 보냄.
transaction.commit();
- 트랜잭션(Transaction)을 지원하는 쓰기 지연
- 엔티티 매니저는 Transaction(트랜잭션)을 커밋하기 전까지 DB에 엔티티를 저장하지 않음.
- 엔티티 매니저 안에 존재하는 SQL 저장소에 INSERT SQL 을 별도로 저장.
- 트랜잭션이 커밋이 될 때 SQL 저장소에 있는 모든 INSERT SQL 을 DB에 요청.
- 플러시(flush)
- 트랜잭션이 커밋될 때 엔티티 매니저는 flush 를 실행.
- 트랜잭션 커밋 요청 -> 엔티티 매니저 flush 실행 -> DB에 쿼리 요청(동기화) -> DB 커밋
- 쓰기 지연 SQL 저장소에 모인 모든 쿼리를 DB에 요청한다. - 트랜잭션 범위 안에서 실행되며, 등록 쿼리를 아무리 생성해도 트랜잭션 커밋을 하지 않으면 DB에 저장되지 않음.
3-3. 엔티티 수정
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 데이터 변경 시 트랜잭션을 시작.
transaction.begin();
// 엔티티 조회(영속성)
Member member1 = em.find(Member.class, "member1");
// 엔티티 데이터 수정
memberA.setName("Developer");
memberA.setPhoneNumber("01052528282");
// 커밋하는 순간 DB에 UPDATE 쿼리를 보냄.
transaction.commit();
- JPA 에서는 엔티티를 수정할 때는 엔티티를 조회하여 데이터를 변경하여 저장.(update() 메소드가 없음)
- 변경 감지 기능을 통해 DB에 자동 반영.(DB Update)
참고!!!
변경 감지는 영속성 컨테스트(엔티티 매니저)가 관리하는 영속 상태의 엔티티에만 적용
- 수정 순서
- 트랜잭션 커밋 요청.(엔티티 매니저 내부에서 먼저 플러시 호출)
- 엔티티 스냅샷과 비교하여 변경된 엔티티를 찾음.
- 변경된 엔티티가 발견되면 Update 쿼리를 생성하여 쓰기 지연 SQL 저장소에 저장.
- 다시 플러시를 호출하면서 쓰기 지연 저장소의 쿼리를 DB에 요청.
- DB 커밋.
- 업데이트의 기본 전략
- JPA의 기본 전략은 모든 엔티티의 필드를 업데이트하는 것이다.
- 모든 필드를 사용하면 수정 쿼리가 항상 같다.
- 동일한 쿼리를 보내면 DB는 이전에 파싱 된 쿼리에 대해서는 재사용을 한다.
3-4. 엔티티 삭제
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 데이터 변경 시 트랜잭션을 시작.
transaction.begin();
// 엔티티 조회(영속성)
Member member1 = em.find(Member.class, "member1");
// 엔티티 데이터 삭제
em.remove(member1);
// 커밋하는 순간 DB에 DELETE 쿼리를 보냄.
transaction.commit();
- 엔티티를 삭제하기 위해서도 먼저 삭제 대상의 엔티티 조회가 필요.
- DB에서 바로 삭제되는 것이 아니라, 영속성 컨텍스트(엔티티 매니저)에서만 제거.
- DELETE 쿼리를 쓰기 지연 SQL 저장소에 저장.
- 트랜잭션 커밋 시 쿼리를 DB 에 요청.
참고!!!
em.remove(member1)를 호출하는 순간 member1은 영속성 컨텍스트에서 제거되어, 더 이상 수정할 수 없다.
참고
- https://data-make.tistory.com/609
- https://ultrakain.gitbooks.io/jpa/content/chapter3/chapter3.4.html
'BackEnd > Spring' 카테고리의 다른 글
[Spring] @Scheduled 어노테이션에서 cron 사용 및 정리 (0) | 2022.08.11 |
---|---|
[Spring] WebClient 정리(API 통신) (0) | 2022.08.07 |
[Spring] JPA 정리 (0) | 2022.07.22 |
[Spring] 의존성 주입(DI) 시 생성자 주입(Constructor Injection)을 사용해야하는 이유 (2) | 2022.07.17 |
[SpringBoot] 스프링부트 + 슬랙(slack bot) 연동하기 (4) | 2022.07.10 |