영속성 관리는 JPA의 핵심 개념으로, 객체와 데이터베이스 사이의 상태를 동기화하여 애플리케이션의 일관성과 성능을 보장한다.
영속성이란?
● 애플리케이션이 종료되어도 데이터가 사라지지 않고 유지되는 성질을 말한다.
● JPA에서는 메모리 상의 객체를 데이터베이스에 영구 저장하고, 반대로 DB의 데이터를 객체로 조회·관리하기 위해 영속성 컨텍스트라는 것을 사용한다.
영속성 컨텍스트(Persistence Context)
● 영속성 컨텍스트는 entity를 관리하는 JPA의 내부 메모리 상의 공간
● 자바 객체와 DB 테이블 간의 상태를 동기화하는 중간 다리 역할
// 웹서버가 올라오는 시점에 DB 당 하나만 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-basic");
// 요청이 올때마다 생성, 쓰레드 간 공유 하지 않는다
EntityManager em = emf.createEntityManager();
MemberEntity member = em.find(MemberEntity.class, 1L);
● 기본 자바 환경에서 영속성 컨텍스트는 EntityManager가 생성될 때 함께 만들어진다.
● EntityManager를 통해서 영속성 컨텍스트에 접근할 수 있다.
● 스프링 프레임워크 같은 컨테이너 환경에서 EntityManger와 PersistenceContext는 N:1의 관계를 갖는다.
● 영속성 컨텍스트를 여러 개의 EntityManager가 공유할 수 있다는 의미이다.
● 스프링은 트랜잭션 단위로 하나의 영속성 컨텍스트를 생성하고, 그 범위 안에서 사용하는 모든EntityManger 호출이 동일한 영속성 컨텍스트에 접근하도록 만든다.
스프링은 @PersistenceContext 또는 @Autowired를 통해 EntityManager를 주입할 때, proxy 객체를 주입한다.
트랜잭션 시작 시점에서 스프링이 먼저 EntityManager를 만든 후 ThreadLocal에 저장해두고, proxy는 이 ThreadLocal을 참고한다.
따라서 한 트랜잭션 범위 내에서 주입된 여러 EntityManager proxy는 같은 EntityManager를 가리키게 된다.
영속성 컨텍스트의 기능을 정확히 이해하려면 먼저 Entity의 생명주기를 알아야 한다.
Entity 생명주기
비영속 (new/transient)
● 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
// 객체를 생성한 상태(비영속)
MemberEntity member = new MemberEntity();
member.setId(1L);
member.setName("JPA");
영속 (managed)
● 영속성 컨텍스트에서 관리되는 상태
● entity의 변경 사항이 감지되고, flush할 때 DB에 반영 된다.
● INSERT, UPDATE, DELETE 쿼리를 직접 작성할 필요가 없다.
tx.begin();
MemberEntity member = new MemberEntity();
member.setId(1L);
member.setName("JPA");
// 객체를 저장한 상태(영속)
em.persist(member);
tx.commit();
준영속(detached)
● 영속 상태의 entity가 영속성 컨텍스트에서 분리된 상태
● 영속성 컨텍스트가 제공하는 기능을 더 이상 사용하지 못한다.
● 준영속 상태로 만드는 3가지 방법
em.detach(member); // 영속성 컨텍스트에서 제거
em.clear(); // 영속성 컨텍스트를 완전히 초기화
em.close(); // 영속성 컨텍스트를 종료
삭제(removed)
● 영속성 컨텍스트에서 내에서 삭제 상태로 변경
● 실제로 DB에 DELETE가 반영되는 것은 트랜잭션이 commit될 때 적용
em.remove(member);
영속성 컨텍스트의 주요 기능
Entity 조회
1차 캐시란?
@Id | Entity |
1 | member1 |
2 | member2 |
● 영속 상태의 엔티티는 1차 캐시에 저장된다.
● 1차 캐시는 영속성 컨텍스트 내부 Map으로 Key에 식별자, Value에 Entity로 저장한다.
● 1차 캐쉬는 한 트랜잭션 안에서만 효과가 있기 때문에, 성능 이점이 그렇게 크지는 않다.
1차 캐시에서 조회
● find의 경우 먼저 1차 캐시에 있는지 조회한다.
● 1차 캐시에 존재하는 경우 DB에서 조회할 필요가 없으므로, SELECT 쿼리가 나가지 않는다.
MemberEntity member = new MemberEntity();
member.setId(1L);
member.setName("JPA");
// 1차 캐시에 저장된 상태
em.persist(member);
// 1차 캐시에서 조회
em.find(MemberEntity.class, 1L);
데이터베이스에서 조회
1. 1차 캐시에 찾고자 하는 entity가 없다.
2. DB에서 조회한다.
3. 1차 캐시에 저장한다.
4. 반환한다.
영속 엔티티의 동일성 보장
MemberEntity member1 = em.find(MemberEntity.class, 1L);
MemberEntity member2 = em.find(MemberEntity.class, 1L);
System.out.println(member1 == member2); // true
● 영속성 컨텍스트 내부의 캐시(Map 구조)덕분에, 같은 PK(primary key)의 엔티티는 항상 같은 자바 객체 인스턴스로 반환되도록 설계
● 1차 캐시로 애플리케이션 차원에서 트랜잭션 격리 수준을 REPEATABLE READ 보장
Entity 등록
트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
● 영속성 컨텍스트의 생명 주기는 트랜잭션 범위에 따라 움직이기 때문에, JPA는 반드시 트랜잭션 안에서 동작해야한다.
EntityTransaction tx = em.getTransaction();
tx.begin();
MemberEntity member1 = new MemberEntity();
member1.setId(1L);
member1.setName("JPA1");
em.persist(member1);
MemberEntity member2 = new MemberEntity();
member2.setId(2L);
member2.setName("JPA2");
em.persist(member2);
// 아직 INSERT SQL을 DB에 보내지 않는다.
tx.commit(); // commit하는 순간 DB에 INSERT SQL 전송
1. persist()를 통해 member 객체를 1차 캐시에 저장한다.
.
2. 비영속에서 영속 상태가 된다.
3. 동시에 INSERT SQL에 필요한 정보를 쓰기 지연 저장소에 등록한다.
4. JpaTransactionManager가 트랜잭션 commit을 수행한다.
5. 내부적으로 entityManager가 flush를 호출하여 모아두었던 SQL을 실행한다.
6. JDBC connection의 commit을 호출하여 DB 트랜잭션을 commit 한다.
스프링의 트랜잭션 commit과 DB의 트랜잭션 commit의 차이를 정확히 인지해야 한다.
flush 란 무엇인가?
● 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 작업
● JPA는 트랜잭션 단위의 일관성을 위해 commit 직전까지 flush를 지연시키는 전략을 취한다.
● 그러나 경우에 따라 동기화를 앞당기기 위해 flush를 호출할 수도 있다.( JPQL 쿼리를 실행하는 경우)
● flush는 DB 트랜잭션을 commit하지는 않는다.
● 따라서 SQL은 실행되지만 DB에 진짜로 확정 된 것은 아니므로, rollback시 여전히 되돌릴 수 있다.
● flush는 영속성 컨텍스트를 비우지 않는다.
● 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화하는 것이다.
영속성 컨텍스트를 flush 하는 방법
● em.flush() 직접 호출
● 트랜잭션 commit하면 flush 자동 호출
● JPQL 쿼리 실행하면 flush 자동 호출
JPQL 쿼리를 실행할 때는 왜 flush가 자동으로 호출될까?
em.persist(member1);
em.persist(member2);
List<MemberEntity> members = em.createQuery("select m from MemberEntity m",
MemberEntity.class)
.getResultList(); // flush 먼저 실행하고 JPQL 실행
● JPQL은 영속성 컨텍스트의 1차 캐시를 무시하고, 오직 DB에 있는 데이터만 조회하기 때문이다.
● 따라서 DB와의 데이터 일관성을 확보하기 위해 JPQL 실행 직전 flush를 자동으로 수행한다.
flush mode
em.setFlushMode(FlushModeType.AUTO);
FlushModeType.AUTO (Default) | commit이나 쿼리를 실행할 때 flush |
FlushModeType.COMMIT | commit할 때만 flush |
Entity 수정
● JPA에서 엔티티의 상태가 변경될 때마다, UPDATE SQL이 쓰기 지연 저장소에 저장된다면, flush 때 DB 통신 비용이 증가하고 성능에 안 좋은 영향을 준다.
● 이러한 문제를 해결하기 위해 JPA는 Dirty Checking을 제공한다.
변경 감지(Dirty Checking)
● 영속성 컨텍스트가 관리 중인 entity의 변경 사항을 자동으로 추적하여 DB에 반영한다.
● 영속성 컨텍스트에 엔티티를 저장할 때, 최초 상태(snapshot)을 기록하여 비교한다.
● 매번 UPDATE SQL을 보낼 필요 없이, 트랜잭션이 종료될 때 최종 변경 사항을 반영하면 된다.
● 따라서 flush할 때 Dirty Checking을 한 번 수행하여, 변경된 필드가 있을 때만 UPDATE SQL을 생성한다.
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
MemberEntity member = em.find(MemberEntity.class, 1L);
member.setName("JPA2");
tx.commit();
● 별도의 update같은 기능이 필요없다.
● 영속 상태인 엔티티의 필드 값을 변경하면 자동으로 감지가 되고, flush때 DB에 반영된다.
Dirty Checking 내부 동작 원리
1. flush() 호출
2. 변경 감지
3. 변경 감지 후 변경된 필드가 있다면, UPDATE SQL 생성하여 쓰기 지연 저장소에 저장
4. 쓰기 지연 저장소의 SQL을 DB에 전송
5. JDBC connection의 commit을 호출하여 DB 트랜잭션을 commit
Entity 삭제
MemberEntity member = em.find(MemberEntity.class, 1L);
em.remove(member);
● entity를 삭제하기 위해서 영속성 컨텍스트 1차 캐시에 entity가 존재해야 한다.
● remove() 호출 후 entity의 상태는 MANAGED에서 DELETED로 변경된다.
● commit 후 flush할 때, 데이터베이스에 DELETE SQL이 전송된다.
'springboot > jpa' 카테고리의 다른 글
JPA 기본키(PK) 매핑 전략 (0) | 2025.06.23 |
---|---|
JPA 필드와 컬럼 매핑 (0) | 2025.06.23 |
JPA DB 스키마 자동 생성 이상과 현실 (0) | 2025.06.22 |
JPA 클래스와 테이블 매핑 @Entity, @Table (1) | 2025.06.22 |
JPA를 왜 사용해야 하는가? (0) | 2025.06.18 |