자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

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. 먼저 1 캐시에서 찾는다.
  2. 없으면 DB에서 찾는다. 그리고 1 캐시에 저장한다.
    준영속 엔티티는 반드시 식별자 값을 가진다. 이전에 영속화 한적이 있기 때문

  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. 1 캐시에서 찾는다.
  2. 없으면 DB에서 찾는다. 그리고 1 캐시에 저장한다.

  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

+ Recent posts