- [JPA] 2. 영속성 컨텍스트(Persistence Context), flush, 준영속 상태2025년 01월 10일 15시 58분 51초에 업로드 된 글입니다.작성자: nickhealthy
영속성 컨텍스트란?
영속성 컨텍스트는 JPA에서 매우 중요한 개념으로, 엔티티를 영속성 상태로 관리 및 저장하는 환경이다. `EntityManager`를 통해 영속성 컨텍스트에 접근하며, 데이터베이스와의 상호작용을 효율적으로 관리한다.
💡영속성이란?
영속성은 데이터나 객체가 일시적인 메모리 내에 머무르는 것이 아니라, 영구적으로 저장되고 유지되는 특성을 의미한다.
프로그래밍에서 영속성은 주로 데이터가 애플리케이션이 종료되더라도 데이터베이스, 파일 시스템, 영속성 저장소와 같은 외부 스토리지에 저장되어 지속적으로 접근할 수 있는 상태를 나타낸다.
엔티티의 생명주기(상태)
- 비영속 (new/Transient): 영속성 컨텍스트와 관계가 없는 상태로, 단순히 메모리에만 존재하는 상태이다.
// JPA와 아무 관계 없는 비영속 상태 Member member = new Member(); member.setId(100L); member.setName("HELLO");
- 영속 (Persistent): 영속성 컨텍스트에 관리되는 상태로, 데이터베이스와 동기화된다.
- `em.persist()` 호출을 통해 엔티티가 영속성 컨텍스트에 관리되게 된다.(아직 데이터베이스에 저장 X)
// JPA와 아무 관계 없는 비영속 상태 Member member = new Member(); member.setId(100L); member.setName("HELLO"); // 영속 상태 em.persist(member);
- 준영속 (Detached): 영속성 컨텍스트에서 분리된 상태로, 더 이상 데이터베이스와 동기화되지 않는다.
- 영속성 컨텍스트에서 분리된 상태로 엔티티를 더이상 관리하지 않는다.
// JPA와 아무 관계 없는 비영속 상태 Member member = new Member(); member.setId(100L); member.setName("HELLO"); em.persist(member); // 영속 상태 em.detach(member); // 엔티티를 영속성 컨트스트에서 분리, 준영속 상태
- 삭제 (Removed): 삭제된 상태로, 트랜잭션이 커밋되면 데이터베이스에서도 삭제된다.
em.remove(member); // 객체를 삭제한 상태(삭제)
영속성 컨텍스트의 생명주기 영속성 컨텍스트의 주요 특징
엔티티 관리
설명: 영속성 컨텍스트는 엔티티의 상태를 관리하고, 데이터베이스와의 동기화를 책임진다.
트랜잭션 스코프
설명: 트랜잭션 범위 내에서 영속성 컨텍스트는 엔티티를 유지하며, 트랜잭션이 종료되면 변경 사항을 데이터베이스에 반영하거나 롤백한다.
1차 캐시
설명: 영속성 컨텍스트 내의 엔티티는 1차 캐시에 저장되어 중복 쿼리 실행을 방지한다.
1. 캐시를 사용하는 경우(SELECT, 조회 쿼리가 발생하지 않는 경우)
`em.persist(member)` 메서드를 통해 영속성 컨텍스트에 저장했다.
이후 `em.find(Member.class, 101L)` 메서드를 통해 영속성 컨텍스트 안에서 데이터를 조회했기 때문에 캐시 데이터를 사용하게 된다. 따라서 SELECT 조회문이 발생하지 않게 된다.
public static void main(String[] args) { ... tx.begin(); // 트랜잭션 시작 // 비영속 Member member = new Member(); member.setId(101L); member.setName("HELLO"); System.out.println("=== BEFORE ==="); em.persist(member); // 영속(영속성 컨텍스트에만 저장) System.out.println("=== AFTER ==="); // 조회(SELECT) 쿼리가 발생하지 않음 Member findMember = em.find(Member.class, 101L); System.out.println("findMember.getId() = " + findMember.getId()); System.out.println("findMember.getName() = " + findMember.getName()); tx.commit(); // 커밋 이후 실제 데이터베이스에 저장 ... }
결과
- INSERT 하는 쿼리만 존재한다. `find()` 메서드를 사용했지만 SELECT(조회) 쿼리는 존재하지 않는다.
- 추가로, BEFORE, AFTER 출력문 사이에서 `em.persist()` 메서드를 사용했지만, 쿼리 실행이 되지 않는 것을 확인할 수 있다. 이는 JPA가 트랜잭션 컷밋이 발생해야지만 실제 데이터가 반영된다는 것을 알 수 있다.
- 또한 `findMember.getId()`, `findMember.getName()` 조회되는 이유는 위와 동일한 이유로 아직 데이터베이스에 반영되지 않았지만 캐시된 영속성 컨텍스트에서 조회가 가능하기 때문이다.
결과1 위의 내용을 도식화하면 다음 그림과 같다.
결과 도식화 2. 캐시를 사용하지 않는 경우(SELECT, 조회 쿼리가 발생하는 경우)
- 이번에는 조회 쿼리가 발생하는 경우인데, 앞의 코드와 다르게 `em.persist()`를 호출하는 부분이 없다. 즉, 영속성 컨텍스트에 저장하는 과정이 빠져있고, 이미 앞전 예제를 통해 `101L` 데이터는(PK 키) 데이터베이스에 저장되어 있다. 따라서 영속성 컨텍스트에서 조회하는 것이 아닌 데이터베이스에 직접 조회해야 하므로 SELECT 쿼리를 사용하게 된다. 단, SELECT 쿼리를 한번만 사용하게 되는데 이미 한번 조회해 온 데이터는 영속성 컨텍스트에 다시 캐시 형태로 존재하게 되므로 두 번째 `find()` 호출은 캐시를 사용하게 된다.
public static void main(String[] args) { ... tx.begin(); // 트랜잭션 시작 Member findMember = em.find(Member.class, 101L); Member findMember = em.find(Member.class, 101L); tx.commit(); // 커밋 이후 실제 데이터베이스에 저장 ... }
결과
결과 위의 내용을 도식화하면 다음 그림과 같다.
결과 도식화 사실 1차 캐시는 그렇게 큰 도움이 안 된다. 왜냐하면 한번의 트랜잭션 당 하나의 엔티티 매니저를 생성하고 종료하게 된다. 따라서 1차 캐시도 한번의 요청 이후 엔티티 매니저를 반환할 때 삭제되기 때문에 조회하는 복잡한 비즈니스 로직이 없는 한 큰 도움은 안 된다.
또한 1차 캐시는 개별적인 엔티티 매니저가 관리하는 캐시라고 하면, 애플리케이션 전반적으로 데이터를 공유해서 사용하는 캐시는 JPA나 하이버네이트에선 2차 캐시라고 한다.
쓰기 지연
설명: 쓰기 지연은 데이터를 즉시 데이터베이스에 반영하지 않고, 트랜잭션이 종료되거나 명시적으로 `flush` 호출이 이루어질 때까지 변경 사항을 영속성 컨텍스트 내에서 보류하는 전략이다. 또한 필요한 시점에 데이터베이스에서 데이터를 가져오는 '지연 로딩'도 지원한다.
예제
public static void main(String[] args) { ... tx.begin(); // 트랜잭션 시작 // 비영속 Member member1 = new Member(150L, "A"); Member member2 = new Member(160L, "A"); // 영속 em.persist(member1); em.persist(member2); // 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다. System.out.println("======================="); tx.commit(); // 커밋 이후 실제 데이터베이스에 저장 ... }
결과
`System.out.println`으로 출력했던 부분까지 INSERT 쿼리가 나가지 않는 것을 확인할 수 있다.
결과 위의 내용을 도식화하면 다음 그림과 같다.
[커밋하기 전]
결과 도식화1 [커밋 이후]
결과 도식화2 변경 감지(Dirty Checking)
설명: 영속성 컨텍스트는 엔티티의 상태 변화를 감지하여 데이터베이스에 필요한 변경사항을 반영한다.
별도로 업데이트 같은 구문이 없어도 Setter를 통해 데이터가 수정된다.
예제
public static void main(String[] args) { EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); transaction.begin(); // [트랜잭션] 시작 // 영속 엔티티 조회 Member findMember = em.find(Member.class, member.getId()); // 영속 엔티티 데이터 수정 findMember.setName("JPA2"); transaction.commit(); // [트랜잭션] 커밋 }
결과
별도의 update 쿼리가 없지만 update 문이 발생하는 것을 확인할 수 있다. 실제 데이터베이스에도 변경된 내역이 반영되었다.
결과 이것이 가능한 이유는 영속성 컨텍스트 안에 1차 캐시에 스냅샷을 저장하고 있기 때문이다. 아래 그림과 같이 커밋을 하게 되면 JPA가 최초로 데이터를 저장한 스냅샷과 변경 건에 대해 비교한 후 데이터가 다르면 쓰기 지연 SQL 저장소에 UPDATE 쿼리를 보관하고 있다가 커밋하게 된다.
결과 도식화 플러시(flush)
영속성 컨텍스트에 있는 변경된 엔티티의 상태를 데이터베이스에 반영하는 것을 '플러시(flush)'라고 한다. `flush()` 메서드를 호출하면, 엔티티 매니저가 관리하는 엔티티 객체들의 변경 내용을 즉시 데이터베이스에 반영한다. 트랜잭션 커밋과의 차이점은 트랜잭션을 커밋하면 DB에 영구적으로 반영되는 반면, `flush()`는 변경된 상태를 DB에 반영하는 시점만 의미한다. 즉, 데이터가 반영되긴 하지만 영구적으로 반영된 상태가 아니고, 롤백이 가능하다.
`flush()` 주요 특징
- 변경 감지 및 동기화
- `flush()`는 영속성 컨텍스트에 있는 모든 변경된 엔티티(예: 새로 추가된 엔티티, 수정된 엔티티, 삭제된 엔티티)의 상태를 DB에 반영한다. 이 시점에서 DB와의 동기화가 이루어진다.
- 트랜잭션 커밋과 독립적
- 트랜잭션은 나중에 커밋해야 하며, 커밋 시점에 DB에 영구적인 저장이 이루어진다. 반면, `flush()`는 잠정적으로 DB에 반영된다.
- 지연 저장소 처리
- 영속성 컨텍스트에서 연관된 엔티티들이 지연 처리 될 때, `flush()`를 호출하면 그 시점에 연관된 엔티티들도 DB에 반영된다.
`flush()`와 `commit()`의 차이점
- `flush()`: 엔티티 매니저가 영속성 컨텍스트에 있는 변경 사항을 DB에 즉시 반영한다. 하지만 트랜잭션을 커밋하지 않으므로, 롤백 시 반영된 변경 사항은 취소될 수 있다.
- `commit()`: 트랜잭션을 완료시키고 DB에 영구적으로 반영한다. 트랜잭션을 커밋하면, 그 안의 모든 변경 사항은 DB에 저장된다.
준영속 상태(Detached State)
JPA에서 준영속 상태는 엔티티 객체가 더 이상 영속성 컨텍스트에 의해 관리되지 않는 상태를 의미한다. 즉, 엔티티가 영속성 컨텍스트에서 분리된 상태로, 이 객체는 더 이상 변경 사항을 추적하거나 자동으로 DB에 반영되지 않는다.
준영속 상태로 만드는 방법
- `em.detach(entity)`: 특정 엔티티를 영속성 컨텍스트에서 분리시킬 수 있다. 이 메서드를 호출하면 해당 엔티티는 준영속 상태로 전환된다.
- `em.clear()`: 영속성 컨텍스트에 관리되는 모든 엔티티를 분리시킨다. 즉, 모든 엔티티가 준영속 상태로 전환된다.
- `em.close()`: `EntityManager`를 닫는 `close()` 메서드를 호출하면 해당 `EntityManager`는 더 이상 엔티티를 관리하지 않게 된다. 하지만 이 방법은 일반적으로 트랜잭션 종료와 연관되므로, 상황에 맞게 사용해야 한다.
1. 예제 - detach() 메서드 사용
public static void main(String[] args) { EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); transaction.begin(); // [트랜잭션] 시작 // 영속 엔티티 조회(영속 상태) Member findMember = em.find(Member.class, member.getId()); // 영속 엔티티 데이터 수정 findMember.setName("JPA2"); System.out.println("==============="); // 주의!! 엔티티는 member가 아니라 findMember 임 em.detach(findMember); transaction.commit(); // [트랜잭션] 커밋 }
결과
위의 변경 감지 코드와 거의 유사한 코드인데 `em.detach()` 부분이 추가되었다. `em.find()` 메서드를 통해 `findMember` 엔티티는 영속성 컨텍스트에서 관리 대상이 되었지만, 곧바로 `em.detach()` 메서드 호출을 통해 영속성 컨텍스트에서 분리(더 이상 JPA에서 관리하지 않는 엔티티 상태)가 되었다. 따라서 `find()` 메서드에 의해 조회(SELECT) 쿼리만 발생한 것을 확인할 수 있다. UPDATE 쿼리는 발생하지 않는다.
추가로 확인할 부분은 `em.find` 메서드는 조회 쿼리를 트랜잭션 커밋 전에 한다는 것을 확인할 수 있다.
(`System.out.println` 콘솔에 나타나는 시점으로 확인 가능)결과 2. 예제 - clear() 메서드 사용
public static void main(String[] args) { EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); transaction.begin(); // [트랜잭션] 시작 // 영속 엔티티 조회(영속 상태) Member findMember1 = em.find(Member.class, member.getId()); System.out.println("==============="); // 영속성 컨텍스트에서 관리하는 모든 엔티티를 초기화 em.clear(); // 초기화 작업으로 인해 다시 동일한 엔티티를 조회했을 때, 1차 캐시 조회가 아닌 SELECT 쿼리로 조회하는 것을 확인할 수 있다. Member findMember2 = em.find(Member.class, member.getId()); transaction.commit(); // [트랜잭션] 커밋 }
결과
`em.find`로 엔티티(`findMember1`)를 조회한 뒤 `em.clear()` 메서드 호출을 통해 영속성 컨텍스트를 모두 초기화 시켰다. 그리고 동일한 엔티티를 조회했을 때 SELECT 쿼리가 호출된 것을 확인할 수 있다. `clear()` 메서드를 호출하지 않았으면 영속성 컨텍스트에는 1차 캐시에 동일한 엔티티가 저장되어 있으므로, JPA는 SELECT 쿼리를 두 번 조회하지 않았을 것이다.
결과 다음글이 없습니다.이전글이 없습니다.댓글