이번 포스팅은 영속성 컨텍스트에 대해 설명해보려고 합니다. 영속성 컨텍스트를 배우기 위해 먼저 EntityManagerFactory와 EntityManager를 알아야 합니다.
EntityManagerFactory & EnitityManger란?
EntityManagerFactory는 애플리케이션 전체에 단 하나만 생성되며 영속성 유닛과 연결을 설정하고 관리합니다. 이 과정에서 DB 커넥션 풀을 생성하게 됩니다. 그리고 클라이언트의 요청이 들어올 때마다 EntityManager를 생성하고, 각 EntityManager는 특정 작업을 수행하기 위해 사용됩니다.
EntityManager는 영속성 컨텍스트를 관리하며, 이를 통해 엔티티 객체의 상태를 추적하고, 트랜잭션이 시작되는 시점에 EntityManager는 커넥션 풀에서 Connection을 얻어와 데이터베이스와의 통신을 시작합니다.
EntityManagerFactory는 애플리케이션의 시작 시점에서 DB 커넥션 풀을 생성하여 초기 생성 비용이 많이 드는 편입니다. 반면에 EntityManager의 생성 비용은 상대적으로 적어 필요할 때마다 EntityManagerFactory를 통해 EntityManager를 생성합니다.
EntityManagerFactory는 여러 스레드가 동시에 접근해도 안전한 스레드 세이프(Thread Safe)한 구조를 가지고 있습니다. 이는 여러 스레드 간에 동시에 사용되어도 안전하다는 것을 의미합니다. 그러나 EntityManager는 트랜잭션 범위 내에서만 유효하며, 여러 스레드가 동시에 접근할 경우 동시성 문제가 발생할 수 있습니다. 따라서 EntityManager는 절대로 공유되어서는 안 됩니다.
영속성 컨텍스트(Perisistence Context)
엔티티(Entity)를 저장하고 관리하는 환경을 의미합니다. 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다고 볼 수 있습니다. 이를 통해 애플리케이션은 엔티티의 생명주기를 관리하고, 변경 내용을 추적하여 실제 데이터베이스에 반영할 수 있습니다.
엔티티의 생명주기
엔티티의 생명주기는 영속성 컨텍스트에서 관리되는 엔티티의 상태를 나타냅니다. 주로 4단계로 구분됩니다.
1. 비영속 (new/transient): 영속성 컨텍스트와 관계가 없는 상태
2. 영속 (managed): 영속성 컨텍스트에 저장된 상태
3. 준영속 (detached): 영속성 컨텍스트에 저장되어 있다가 분리된 상태
4. 삭제 (removed): 삭제된 상태
순수 JPA로 영속성 컨텍스트를 구현하면 다음과 같습니다.
// EntityManagerFactory를 사용하여 영속성 컨텍스트를 설정합니다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
// EntityManager를 생성합니다.
EntityManager em = emf.createEntityManager();
// 트랜잭션을 관리하기 위한 EntityTransaction을 얻습니다.
EntityTransaction tx = em.getTransaction();
// 트랜잭션을 시작합니다.
tx.begin();
try {
// ... 여기에 트랜잭션 범위 내에서 수행할 엔티티 로직을 작성합니다.
// 트랜잭션을 커밋하여 변경사항을 반영합니다.
tx.commit();
} catch (Exception e) {
// 예외가 발생한 경우 트랜잭션을 롤백합니다.
tx.rollback();
} finally {
// EntityManager를 닫아 리소스를 해제합니다.
em.close();
// EntityManagerFactory를 닫아 사용이 끝난 영속성 컨텍스트를 해제합니다.
emf.close();
}
영속성 컨텍스트의 기능을 설명하기 위한 코드들은 위 코드의 트랜잭션 범위 내에서 구현된 것으로 생략하여 설명하겠습니다.
1. 1차 캐시
1차 캐시는 EntityMnager가 관리하는 영속성 컨텍스트 내부에 있는 첫 번째 캐시를 의미합니다. 1차 캐시의 생명주기는 트랜잭션 범위 내에 만 유지되며, 범위를 벗어나면 1차 캐시는 삭제됩니다.
영속성 컨텍스트를 저장하기 위해선 'em.perist()' 메서드를 사용하여 저장합니다. 이때 데이터베이스에 Mermer 객체는 아직 저장되지 않은 상태로 영속성 컨텍스트(1차 캐시)에서만 저장되어 있습니다. 이후 1차 캐시에 저장된 Member 객체를 조회합니다.
Member member = new Member();
member.setUsername("user123");
// 엔티티 영속화
em.perist(member);
// 1차 캐시에서 조회
Member findMember = em.find(Member.class, member.getId());
데이터베이스에서 데이터를 조회하기 위해 Member 테이블 정보 중 pk가 member.getId()에 해당하는 값을 찾아오는 sql을 수행하게 됩니다. 그러나 1차 캐시로 인해 데이터베이스에 접근하지 않고도 Member 엔티티를 가져올 수 있게 됩니다.
이러한 이유는 JPA는 조회를 할 때 먼저 1차 캐시에 해당 엔티티가 있는지 탐색하고 있다면 1차 캐시에서 꺼내오고, 없다면 데이터베이스에 접근하여 찾아오기 때문입니다. 이를 조회 과정으로 풀어쓰면 다음과 같습니다.
조회 과정
- em.find()를 통해 1차 캐시에 해당 엔티티가 있는지 탐색 (엔티티가 있다면 해당 객체 return)
- 1차 캐시에 엔티티가 없다면 데이터베이스에 접근하여 값을 탐색
- 데이터베이스를 통해 가져올 경우 1차 캐시에 저장 후, 엔티티를 반환
💡 영속성 컨텍스트 참고
- 영속성 컨텍스트(1차 캐시)는 서로 공유하지 않는다.
- 10명의 클라이언트가 요청을 보내면 EntityManager도 10개 생성되며, 1차 캐시도 10개가 생성된다.
- EntityManager는 보통 DB 트랜잭션 단위로 생성되며, 트랜잭션이 종료되는 시점에 종료(삭제)된다.
- EntityManager가 종료되면 EntityManager의 1차 캐시에 존재하는 모든 데이터가 삭제된다.
- 트랜잭션의 유지 범위는 짧은 편이기 때문에 1차 캐시를 통해 큰 성능 이점을 얻기보단 컨셉에 이점이 있다.
- 컨셉이라 함은 객체지향적으로 개발할 수 있음과 편의성이 있다.
2. 동일성(Indentity) 보장
JPA는 하나의 트랜잭션 범위 내에 존재하는 동일한 pk(식별자 값)를 가진 엔티티에 대해 동일성을 보장합니다. 여기서 동일성은 값만이 아니라 실제 인스턴스 자체가 같다는 의미를 가집니다(주솟값 동일). 이는 1차 캐시를 통해 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다는 것을 의미합니다.
Member findMember1 = em.find(Member.class, 1L);
Member findMember2 = em.find(Member.class, 1L);
// true 반환
System.out.println(findMember1 == findMember2);
3. 트랜잭션을 지원하는 쓰기 지연
tx.commit() 메서드가 실행되기 전까지 트랜잭션 범위 내 엔티티 값들은 DB에 반영되지 않은 상태입니다. 등록을 위한 sql은 바로 호출되지 않고 따로 쓰기 지연 SQL 저장소에 쿼리를 생성하여 쌓아 둡니다. 이후 tx.commit() 메서드가 호출되는 시점이거나, 강제로 em.flush() 메서드를 호출할 경우 DB에 접속하여 쿼리를 반영합니다.
Member memberA = new Member(1L, "userA");
Member memberB = new Member(1L, "userB");
// 엔티티 영속화 (1차 캐시에 저장, DB 반영은 아직 X)
em.perist(memberA);
em.perist(memberB);
// 트랜잭션 커밋 (엔티티를 DB에 반영)
tx.commit();
💡 플러시(flush)란?
플러시(flush)는 영속성 컨텍스트의 변경 내용을 데이터베이스에 즉시 반영하는 작업입니다. 이때, 1차 캐시에 있는 엔티티들은 여전히 유지됩니다. 즉, 영속성 컨텍스트에 있는 엔티티의 변경 내용은 데이터베이스에 동기화됩니다.
4. 변경 감지(Dirty Checking)
1차 캐시에 저장되어 있는 엔티티를 수정할 경우 영속성 컨텍스트에게 따로 알려주지 않아도 알아서 변경사항을 감지하고, DB에 반영시켜 줍니다.
// 엔티티 조회 (DB에 접속하여 엔티티를 가져오고, 1차 캐시에 저장)
Member findMember = em.find(Member.class, "userA");
// 엔티티 수정
findMember.setUserName("userH");
// 엔티티 영속화 (필요 없음 why? em.update() 이런 코드가 있어야 하지 않을까?)
// em.persist(findMember);
// 트랜잭션 커밋 (엔티티를 DB에 반영)
tx.commit();
1차 캐시에는 스냅샷이라는 컬럼이 존재합니다. 엔티티가 1차 캐시에 저장될 때, 저장되는 최초 시점의 상태를 스냅샷으로 만들어 보관하게 됩니다. 이후 트랜잭션이 commit 되는 시점에 현재 엔티티의 상태와 스냅샷을 비교하고 일치하지 않다면 이를 감지하여 DB에 반영해줍니다. 이를 변경 감지라고 합니다.
변경 감지 순서
- 트랜잭션 commit이 되는 시점에 EntityManager 내부에서 플러시(flush)를 호출합니다.
- 현재 엔티티와 스냅샷을 비교하여 변경된 엔티티를 감지합니다.
- 변경된 엔티티가 있다면 update 쿼리를 쓰기 지연 SQL 저장소에 생성합니다.
- 트랜잭션 범위가 끝나는 시점에 쓰기 지연 SQL 저장소에 보관한 쿼리를 실행합니다.
자바 ORM 표준 JPA 프로그래밍 - 기본편 | 김영한 - 인프런
김영한 | JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 실무에서도
www.inflearn.com
'Spring > JPA' 카테고리의 다른 글
프록시(Proxy) (0) | 2024.04.28 |
---|---|
상속관계 매핑 (0) | 2024.04.24 |
연관관계 매핑 정리 (0) | 2024.04.23 |
엔티티 매핑(Entity Mapping) - 기본 키 (1) | 2024.04.22 |
엔티티 매핑(Entity Mapping) - 객체와 테이블, 필드와 컬럼 (1) | 2024.04.21 |