소스
https://github.com/rkwhr0010/jpa
엔티티 매니저 팩토리와 엔티티 매니저
https://openjpa.apache.org/builds/1.0.2/apache-openjpa-1.0.2/docs/manual/img/jpa-arch.png
엔티티 매니저 팩토리는 스레드 세이프티 하다.
엔티티 매니저는 반대로 절대 다른 스레드 간 공유하면 안된다.
대부분 JPA 구현체들은 엔티티매니저팩토리를 만들 때 커넥션 풀도 같이 만든다.
persistence.xml 설정을 보면 알 수 있다.
영속성 컨텍스트(Persistence context)
영속성 말 그대로 엔티티(데이터)가 영구히 저장되는 저장소를 의미한다.
1차 캐새라고도 불린다.
영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어진다.
엔티티 매니저를 통해 영속성 컨텍스트를 조작할 수 있다.
하나의 영속성 컨텍스트는 여러 엔티티 매니저가 접근 할 수도 있다.
엔티티 생명주기
https://thorben-janssen.com/entity-lifecycle-model/
비영속 (Transient/ new)
데이터베이스 및 JPA 관련 기능에 대한 연결이 없는 기본 Java 객체
영속(Managed)
모든 변경 사항을 감지하고 지속성 컨텍스트를 플러시할 때 필요한 SQL INSERT 또는 UPDATE 문을 생성
저장된 객체도 영속 상태
조회된 상태도 영속 상태, 참고로 JPQL로 조회한 객체도 영속 상태다.
EntityManager.merge 메소드를 호출하여 분리된 엔티티를 병합할 때도 영속 상태가 된다
준영속(Detached)
영속성 컨텍스트가 관리하던 엔티티가 관리하지 않게 된 상태
영속성 컨텍스트에서 특정 엔티티를 분리하거나
영속성 컨텍스트를 초기화하거나
영속성 컨텍스트를 닫을 때, 남겨진 엔티티는 준영속 상태가 된다.
실제로 사용 시 보통 DB에서 데이터를 조회하고 트랜잭션이 종료되고 나서 반환 받은 엔티티가 준영속 상태이다.
삭제
삭제를 해도 바로 제거되진 않는다. flush 할때 DB에서 삭제된다.
영속성 컨텍스트 특징
영속성 컨텍스트는 엔티티를 식별자 값으로 구분한다. 따라서 영속 상태에선 식별자 값이 반드시 있어야 한다.
영속성 컨텍스트에 저장된 엔티티는 보통 트랜잭션이 커밋될 때 DB에 저장된다.
엔티티를 DB에 반영하는 것을 flush라 한다.
영속성 컨텍스트 장점
1차 캐시, 동일성(==) 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩
조회
https://medium.com/@seonggil/hibernate-second-level-cache-with-ehcache-5d27d6bec387
첫 번째 조회했을 땐, 1차 캐시에 존재하지 않아 DB에서 조회했다. 조회된 결과는 영속영 컨텍스트에 먼저 저장된 후 반환된다.
두 번째 조회했을 땐, 2차 캐시에 존재해 DB에서 조회할 필요가 없다.
두 객체의 동일성 비교를 통해 같은 인스턴스인 것을 확일할 수 있다.
저장
좀 더 명확히 구분되는 결과를 위해 자동 생성 KEY는 제외했다.
이유는 영속성 컨텍스트는 식별자로 엔티티를 구분하는데, 이를 바꿔 말하면 반드시 식별자가 존재해야 영속성 컨텍스트에 저장할 수 있다는 뜻이다.
자동 키 생성 전략이 시퀀스인 경우
DB에 먼저 한 번 접근해서 시퀀스를 받아온 뒤 그 값을 식별자로 엔티티를 영속화한다. 이후 쓰기 지연을 수행한다.
private static void createPersistenceContext(Consumer<EntityManager> consumer) { EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); try { tx.begin(); consumer.accept(em); logger.info("###################### 커밋 ##########################"); tx.commit(); } catch (Exception e) { tx.rollback(); } finally { em.close(); } } private static void save() { createPersistenceContext(em -> { Member member = new Member(); member.setId(1L); member.setName("홍길동"); em.persist(member); member = new Member(); member.setId(2L); member.setName("임꺽정"); em.persist(member); member = new Member(); member.setId(3L); member.setName("우투리"); em.persist(member); }); } |
persist()호출을 하면, DB에 바로 저장하는 것이 아니라 작업 큐에 SQL을 저장해둔다.
그리고 영속성 컨텍스트에 저장한다.
이후 트랜잭션 커밋 시 DB와 동기화 된다.
쓰기 지연은 물리적 데이터베이스 트랜잭션이 짧아져 데이터베이스의 잠금 경합이 줄어드는 효과가 있다.
수정 (변경 감지)
SQL 직접 사용 문제점
일반적으로 직접 SQL로 Update 문을 작성할 시 비즈니스 요구사항이 변경되 추가되는 컬럼이 생기면, 따로 Update 문을 더 만들거나 기존 Update 문을 변경해야 한다.
SQL에 직간접적으로 의존하게 된다.
JPA 수정
엔티티를 조회 후 값을 변경하면 알아서 DB에 반영이 된다. 이를 변경 감지라 한다.
private static void update() { createPersistenceContext(em -> { Member m = em.find(Member.class, 1L); m.setName("아무개"); }); } |
참고용
수정 변경 감지
엔티티를 최초 영속성 컨텍스트에 저장할 때 그 상태를 스냅샷으로 저장을 한다.
그리고 플러시 시점에서 스냅샷을 기반으로 달라진 엔티티를 찾아 업데이트를 한다.
변경 감지는 이처럼 영속성 컨텍스트가 관리하는 상태에만 적용된다.
private static void update() { createPersistenceContext(em -> { Member m = em.find(Member.class, 1L); m.setName("아무개"); em.detach(m); // 영속성 컨텍스트에서 분리하면, 업데이트 반영 안됨 }); } |
JPA 수정 알아둘 것
하나의 변경이라도, 모든 필드를 수정한다.
@Entity public class Member { @Id // @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "MEMBER_ID") private Long id; private String name; //주의사항을 알기 위해 일부러 필드 추가 private String dummy1; private String dummy2; private String dummy3; private String dummy4; |
DB는 바인드 변수를 제외한 모든 쿼리가 같으면, 그 쿼리를 재사용(캐시)한다.
필드가 많거나 수정할 때 너무 많은 데이터를 소비하게 된다면, 그 땐 수정된 데이터만 수정하도록 하는 전략을 사용하면 된다.
@Entity @DynamicUpdate public class Member { @Id // @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "MEMBER_ID") private Long id; private String name; //주의사항을 알기 위해 일부러 필드 추가 private String dummy1; private String dummy2; private String dummy3; private String dummy4; |
일반적으로 30개 이하면, 정적 수정 쿼리가 더 빠르다.
참고로 테이블 컬럼이 30개 이상인 경우는 테이블 설계가 잘못되었을 가능 성이 높다.
보통 정적으로 사용하다, 성능 이슈가 나오면 동적으로 사용하면 된다.
삭제
Member m = em.find(Member.class, 1L); em.remove(m); |
삭제 또한 remove()호출 시 즉시 제거되는 게 아니라 쓰기 지연 SQL 저장소에 등록 후 플러시 시점에 제거 SQL이 수행된다.
단, remove() 호출 이후로는 영속성 상태가 아니라는 것을 명심해야 한다.
플러시
플러시는 영속성 컨텍스트의 변경 내용을 DB에 반영한다.
버퍼나 캐시에서 플러시 호출은 비우는 것이지만,
JPA에선 플러시가 비운다는 뜻이 아니다. 영속성 컨텍스트의 변경 내용을 DB와 동기화 시키는 작업이다.
변경 감지가 동작해 Update 할 엔티티를 찾아 쓰기 지연 SQL에 저장한다.
쓰기 지연 SQL 저장소에 담긴 CUD 쿼리를 DB에 전송한다.
플러시 하는 방법 3 가지
- flush() 직접 호출
- 트랜잭션 커밋 시 자동 호출
- JPQL 쿼리 실행 시 자동 호출
Member member = new Member(); member.setId(1L); member.setName("홍길동"); em.persist(member); member = new Member(); member.setId(2L); member.setName("임꺽정"); em.persist(member); TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class); query.getResultList(); member = new Member(); member.setId(3L); member.setName("우투리"); em.persist(member); |
JPQL 의 경우 영속성 컨텍스트를 안 거치고 직접 DB로 질의를 한다.
이때 영속성 컨텍스트의 엔티티의 변경 사항은 아직 DB에 반영되지 않은 상태다.
따라서 JPQL 실행 전에 자동으로 플러시를 호출한다.
플러시 모드
기본은 AUTO
COMMIT 은 말 그대로 커밋할 때만 호출된다. JPQL은 해당 없다.
준영속
영속성 상태에서 분리된 상태
영속성 컨텍스트가 제공하는 기능을 사용하지 못한다.
크게 3가지
- detach(엔티티) 호출
- clear() 영속성 컨텍스트 초기화
- close() 영속성 컨텍스트 종료
detach()
Member m1 = new Member(); m1.setId(1L); m1.setName("홍길동"); em.persist(m1); Member m2 = new Member(); m2.setId(2L); m2.setName("임꺽정"); em.persist(m2); Member m3 = new Member(); m3.setId(3L); m3.setName("우투리"); em.persist(m3); em.detach(m1); em.detach(m2); em.detach(m3); |
영속성 컨텍스트에 저장된 엔티티들이 detach()로 준영속이 되면서
1차 캐시에서 제거, 쓰기 지연 SQL 저장소에서 제거 등 엔티티 관련된 모든 정보가 제거 된다.
clear()
Member m1 = new Member(); m1.setId(1L); m1.setName("홍길동"); em.persist(m1); Member m2 = new Member(); m2.setId(2L); m2.setName("임꺽정"); em.persist(m2); Member m3 = new Member(); m3.setId(3L); m3.setName("우투리"); em.persist(m3); em.clear(); |
플러시가 호출되기 전에 영속성 컨텍스트를 초기화했으므로 DB에는 아무것도 반영이 안된다.
close()
Member m1 = new Member(); m1.setId(1L); m1.setName("홍길동"); em.persist(m1); Member m2 = new Member(); m2.setId(2L); m2.setName("임꺽정"); em.persist(m2); Member m3 = new Member(); m3.setId(3L); m3.setName("우투리"); em.persist(m3); em.close(); |
일단 플러시가 되서 영속성 컨텍스트 변경사항은 DB에 반영된다.
이후 영속성 컨텍스트가 종료되며 나머지 엔티티는 일반 자바 객체가 된다.
준영속 상태 특징
- 거의 비용속 상태에 가깝다.
- 식별자 값을 가지고 있다.
- 지연 로딩을 할 수 없다.
merge() 병합
준영속 상태 엔티티를 다시 영속 상태로 변경할 때 사용한다.
Member m1 = new Member(); m1.setId(1L); m1.setName("홍길동"); em.persist(m1); Member m2 = new Member(); m2.setId(2L); m2.setName("임꺽정"); em.persist(m2); Member m3 = new Member(); m3.setId(3L); m3.setName("우투리"); em.persist(m3); em.flush(); // DB 동기화 em.clear(); // 영속성 컨텍스트 비우기 (모든 엔티티는 준영속 상태) //준영속 상태 엔티티를 줘서 새로운 영속 상태 엔티티를 받는다. m1 = em.merge(m1); //merge()호출 전에 값을 바꿔도 상관 없다. flush() 시 바뀐 값으로 DB에 반영된다. m3.setName("우투리2"); //주의, 반환 객체를 반드시 사용해야 한다. em.merge(m2); m3 = em.merge(m3); m1.setName("홍길동2"); m2.setName("임꺽정2"); System.out.println("m1 " + (em.contains(m1) ? "관리 중" : "모름")); System.out.println("m2 " + (em.contains(m2) ? "관리 중" : "모름")); System.out.println("m3 " + (em.contains(m3) ? "관리 중" : "모름")); |
머지를 호출해도 반환 받은 엔티티를 사용해야지 그대로 준영속 엔티티를 사용하면 안된다. 위 예제에서 m2 가 해당한다.
과정
- 먼저 1차 캐시에서 찾는다.
- 없으면 DB에서 찾는다. 그리고 1차 캐시에 저장한다.
준영속 엔티티는 반드시 식별자 값을 가진다. 이전에 영속화 한적이 있기 때문
- DB에 있었으므로, 변경 사항을 반영한다.(수정, 삭제)
이 식별자 값으로 DB에서 조회를 하기 때문에 merge() 전에 변경사항도 DB에 잘 반영된다.
비영속 병합
Member m1 = new Member(); m1.setId(1L); m1.setName("홍길동"); Member m2 = new Member(); m2.setId(2L); m2.setName("임꺽정"); Member m3 = new Member(); m3.setId(3L); m3.setName("우투리"); m1 = em.merge(m1); m2 = em.merge(m2); em.merge(m3); m2.setName("변경됨"); m3.setName("실수"); |
준영속일 때과 과정은 같다.
- 1차 캐시에서 찾는다.
- 없으면 DB에서 찾는다. 그리고 1차 캐시에 저장한다.
- DB에서 조회가 안되면, INSERT 를 한다.
조회해서 없으면 INSERT 고, 존재하면 UPDATE(혹은 DELETE) 이다.
merge()는 반드시 반환된 엔티티를 사용해야 한다.
정리
영속성 컨텍스트는 애플리케이션과 DB 사이에서 가상의 DB 역할을 한다.
이 사이에서 1차 캐시, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩 기능을 제공한다.
영속성 컨텍스트에 저장된 엔티티는 플러시를 해야 DB에 반영된다.
따라서 자동으로 플러시를 호출해주는 지점을 잘 파악해야 한다.
일반적으로 트랜잭션
영속 상태 였다가 해제된 상태를 준영속이라 한다.
영속 상태에서 기능을 사용하지 못한다.
'개발 > JPA' 카테고리의 다른 글
06 다양한 연관관계 매핑 (0) | 2024.04.14 |
---|---|
05 연관관계 매핑 기초 (1) | 2024.03.22 |
04 엔티티 매핑 (1) | 2024.02.26 |
02 JPA 소개 (0) | 2023.12.01 |
01 JPA 환경 설정 (0) | 2023.11.23 |