개요
이번 게시글에서는 JPA 내부 구조 및 동작 방식을 파악하는데 중요한 개념인 PersistenceContext(영속성 컨텍스트)에 대해 알아보겠습니다.
1. EntityManagerFactory & EntityManager
JPA 내부 구조를 살펴보면 EntityMangerFactory 객체와 EntityManager 객체가 있습니다.
EntityManagerFactor객체는 고객의 요청이 올 때마다 즉, 각 쓰레드마다 EntityManager 객체를 생성하고 해당 객체는 내부적으로 DB Connection Pool을 활용하여 DB에 연결을 합니다.
위 내용을 코드로 구성해보면 아래와 같습니다.
2. PersistenceContext (영속성 컨텍스트)
- JPA를 이해하는데 가장 중요한 용어
- 물리적인 개념이 아니며 논리적인 개념
- 앞서 소개한 EntityManager를 통해 PersistenceContext에 접근
- EntityManager의 persist 메서드를 통해 엔티티를 저장
- 각 쓰레드마다 EntityManagerFactory에서 EntityManager 객체를 생성하므로 EntityManager와 PersistenceContext는 N:1 관계
2.1 엔티티의 생명주기
PersistenceContext를 이해하기 위해서는 엔티티의 생명주기에 대해서 알아야 합니다.
생명주기 내 엔티티의 상태는 아래와 같이 4 가지로 구분할 수 있습니다.
- new/transiet(비영속): PersistenceContext와 전혀 관계가 없는 새로 생긴 상태
- managed(영속, persist): PersistenceContext에 관리되는 상태
- detached(준영속): PersistenceContext에 저장되었다가 분리된 상태
- removed(삭제): 삭제된 상태
2.1.1 New/Transient (비영속)
- 비영속 상태는 아래와 같이 객체를 생성한 상태이며 아직 EntityManager에 의해 관리되지 않은 상태
// 비영속 (객체를 생성한 상태)
Blogger blogger = Blogger.builder()
.id("jaimemin")
.blogUrl("jaimemin.tistory.com")
.build();
2.1.2 Managed (영속)
- 영속 상태는 EntityManager에 의해 객체가 관리되고 있는 상태
- 단, 영속 상태라고 해서 DML이 바로 DB에 날아가는 것은 아님
- 트랜잭션을 commit 해야지 쿼리가 날아감
EntityManagerFactory entityManagerFactory
= Persistence.createEntityManagerFactory("[PERSISTENCE UNIT]");
EntityManager entityManager = entityManagerFactory.createEntityManager();
// 객체를 저장한 상태 (영속)
entityManager.persist(Blogger.builder()
.id("jaimemin")
.blogUrl("jaimemin.tistory.com")
.build());
2.1.3 Detached (준영속)
- 준영속 상태는 EntityManger에서 분리되어 있는 상태
- EntityManager가 제공하는 기능을 사용 못함
// 엔티티를 EntityManager에서 분리
entityManager.detach(blogger);
2.1.4 Removed (삭제)
- 객체를 삭제한 상태
entityManager.remove(blogger);
2.2 PersistenceContext를 사용함에 따른 장점
- 1차 캐시 기능
- Java Collection과 같이 객체 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연 지원
- Dirty Checking 지원
- Lazy Loading 지원
2.2.1 1차 캐시 기능
- New/Transient 상태에서 Managed 상태가 될 때 EntityManager 내 1차 캐시에 해당 객체를 저장
- 캐시는 Map과 같은 형태이며 id를 key, 객체를 value 값으로 가지는 형태
- 객체를 조회할 때, EntityManager는 우선 캐시를 조회한 후 해당 객체가 캐시에 있을 경우 DB를 거치지 않고 바로 캐시에서 해당 객체를 꺼내서 반환해줌 (속도 향상)
- 만약 데이터베이스에 없을 경우 DB에서 조회한 후 1차 캐시에 객체를 저장한 뒤 해당 객체를 반환하는 프로세스를 거침
- 위와 같은 과정을 보면 성능에 큰 도움을 줄 것 같지만 1차 캐시는 Transaction 단위로만 유지가 됨
- 고객의 요청이 끝날 경우 1차 캐시를 flush 하기 때문에 성능 이점을 누리기 힘든 구조
- 하지만, 비즈니스 로직이 엄청 복잡한 서비스라면 1차 캐시 기능을 통해 성능을 많이 향상할 수 있음
2.2.2 Java Collection과 같이 객체 동일성 보장
- 영속 엔티티는 Java Collection과 유사하게 동작
- 아래와 같이 같은 트랜잭션 내 동일한 쿼리로 조회한 객체는 동일성 유지
Blogger jaimemin = entityManager.find(Blogger.class, "jaimemin");
Blogger jaimemin2 = entityManager.find(Blogger.class, "jaimemin");
log.info(jaimemin == jaimemin2); // true
2.2.3 트랜잭션을 지원하는 쓰기 지연 지원
- 앞서 생명주기에서 영속 상태라고 해서 바로 DB에 DML 쿼리가 날아가는 것이 아니라고 언급했음
- EntityManger의 persist 메서드를 호출할 때마다 DB에 쿼리를 날릴 경우 네트워크 오버헤드가 커질 수 있음
- 따라서, EntityManager는 persist 메서드를 호출할 때마다 SQL 저장소에 쿼리를 쌓아놓은 뒤 트랜잭션을 commit 할 때 쿼리를 한 번에 날림
- transaction.commit() 메서드가 호출되면 내부적으로 아래와 같은 과정을 거침
- commit()이 호출되고
- flush 메서드가 호출되며 JDBC Batch를 통해 SQL 저장소에 쌓인 쿼리들이 DB에 날아가고
- 최종적으로 DB에 반영되도록 commit() 메서드가 호출됨
* 객체를 삭제하는 remove 메서드 또한 트랜잭션이 커밋되는 시점에 반영이 됨
2.2.4 Dirty Checking 지원
- EntityManager에 의해 관리되는 객체는 변경이 자동적으로 감지됨
- 이 말은 즉슨 엔티티를 수정한 뒤에 트랜잭션을 커밋할 경우 굳이 update 쿼리를 직접 호출하지 않더라도 Dirty Checking이 지원되기 때문에 자동으로 호출이 됨
- transaction.commit() 메서드가 호출된 뒤 내부적으로 아래와 같은 과정을 거침
- flush() 메서드가 호출되고
- 엔티티와 기존 스냅샷에 저장된 엔티티를 비교
- 비교했을 때 변경된 내용이 있을 경우 UPDATE 쿼리 생성한 뒤 SQL 저장소에 저장
- flush 메서드가 호출되며 JDBC Batch를 통해 SQL 저장소에 쌓인 UPDATE 쿼리가 DB에 날아가고
- 최종적으로 DB에 반영되도록 commit() 메서드가 호출됨
* SNAPSHOT: 최초로 Entitymanager에 들어온 시점의 객체 상태
2.2.5 Lazy Loading 지원
아래와 같이 객체 내 필드로 또 다른 객체가 있다고 가정
Class Blogger {
private String id;
private String blogUrl;
private IpPort ipPort;
}
- 기존대로라면 불러올 때 Blogger 내 모든 필드를 불러옴
- Lazy Loading이 지원되면 id와 blogUrl 필드가 먼저 불러오고 IpPort 객체에 대한 정보가 필요한 시점에서 IpPort 객체 정보를 불러옴
- 필드가 많을 경우 Lazy Loading 덕분에 성능 최적화
3. Flush 메서드
- 앞서 설명한 엔티티의 생명주기와 PersistenceContext 장점에 flush 메서드가 언급됐었음
- flush 메서드는 EntityManager의 변경 내용을 DB에 반영하는 메서드
- flush는 아래와 같은 상황에서 발생
- 변경 감지 (Dirty Checking)
- 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송 (INSERT, UPDATE, DELETE 쿼리)
3.1 EntityManager를 flush 하는 방법
- flush() 메서드를 직접 호출
- transaction을 커밋하기 전에 DB에 반영되고 싶을 때 해당 방법을 사용하지만 이렇게 사용할 일 거의 없음
- flush 메서드가 호출되더라도 트랜잭션이 끝난 것이 아니기 때문에 1차 캐시는 유지가 되며 EntityManager의 변경내용을 DB와 동기화
- 결국에는 트랜잭션 작업 단위 내에서 실행되면 되므로 commit 직전에만 flush를 하면 됨
- transaction.commit(): flush 자동 호출
- JPQL 쿼리 실행: flush 자동 호출
3.2 flush 모드 옵션
- FlushModeType.AUTO (디폴트 값): commit이나 쿼리 실행 시 flush
- FlushModeType.COMMIT: commit 할 때만 flush
entityManager.setFlushMode(FlushModeType.COMMIT)
출처
자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한 강사님)
'DB > JPA' 카테고리의 다른 글
[JPA] 상속관계 매핑 (2) | 2021.09.07 |
---|---|
[JPA] 다양한 연관관계 매핑 (0) | 2021.08.31 |
[JPA] 연관관계 매핑 간단 정리 (0) | 2021.08.28 |
[JPA] Entity 매핑 정리 (0) | 2021.08.23 |
JPA를 학습해야하는 이유 (1) | 2021.08.16 |