[JPA 기본편] 2. 영속성 관리

HJ·2024년 2월 21일
0

JPA 기본편

목록 보기
2/10

김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 보고 작성한 내용입니다.


1. 엔티티 매니저와 영속성 컨텍스트

엔티티 매니저 팩토리를 통해서 고객의 요청이 올 때마다 앤티티 매니저를 생성합니다.

엔티티 매니저 팩토리는 하나만 생성해서 애플리케이션 전체에서 공유하고, 앤티티 매니저는 내부적으로 DB 커넥션을 사용해서 DB 를 사용하게 됩니다.

EntityManger.persist() 의 정확한 의미는 DB에 저장하는 것이 아니라 영속성 컨텍스트를 통해서 엔티티를 영속화 한다는 의미, 즉 엔티티를 영속성 컨텍스트에 저장한다는 의미입니다.

영속성 컨텍스트란 엔티티를 영구 저장하는 환경이라는 뜻이며, 엔티티 매니저를 통해 영속성 컨텍스트에 접근할 수 있습니다.

영속성 컨텍스트가 있음으로 애플리케이션이랑 DB 사이에 중간 계층이 하나 생성된 것으로 볼 수 있습니다. 이것으로 얻을 수 있는 이점은 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연로딩이 있습니다.




2. 엔티티의 생명 주기

  1. 비영속( new / transient ) : 최초에 객체를 생성한, 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태입니다.

  2. 영속( managed ) : EntityManager.persist() 를 호출했을 때 되는 상태로 영속성 컨텍스트에서 관리되는 상태입니다.

  3. 준영속( detached ) : 영속성 컨텍스트에 저장되었다가 분리된 상태입니다.

  4. 삭제( removed ) : 삭제된 상태

[ 비영속 ]

// 비영속 상태
Member member = new Member();  
member.setId("member1");  
member.setUsername(“회원1);

Member 객체를 생성하고 EntityManager 에 아무것도 넣지 않은, 즉 객체를 생성만 해서 JPA 와 전혀 관계가 없는 상태를 비영속 상태라고 합니다.

[ 영속 ]

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

//객체를 저장한 상태( 영속상태 )
em.persist(member);

EntityManger 안에는 영속성 컨텍스트가 존재합니다. 객체를 생성한 후, 엔티티 매니저를 가져와서 엔티티 매니저에 persist() 로 객체를 집어넣으면 엔티티 매니저 안에 있는 영속성 컨텍스트에 객체가 들어가면서 영속 상태가 됩니다.

영속 상태가 되었다고 해도 DB 에 저장되는 것은 아닙니다. 트랜잭션이 commit 될 때 영속성 컨텍스트에 있는 엔티티들에 대한 쿼리가 DB 에 날라가 저장되게 됩니다.




3. 1차 캐시

3-1. 저장

영속성 컨텍스트는 위의 그림처럼 내부에 1차 캐시라는 것을 가지고 있습니다. 1차 캐시를 보면 @Id 가 있고 Entity 가 있는 것을 확인할 수 있는데 DB 에 PK 로 매핑한 것이 key 가 되고 엔티티 객체 자체가 value 가 됩니다.

//엔티티를 생성한 상태(비영속)  
Member member = new Member();  
member.setId("member1");  
member.setUsername("회원1");

//엔티티를 영속  
em.persist(member);

Member 객체를 생성하고 id 를 member1 으로 지정한 후에 persist() 를 호출하면 해당 엔티티는 영속성 컨텍스트의 1차 캐시에 저장됩니다.


3-2. 조회

[ 1차 캐시에 존재하는 경우 ]

Member member = new Member();  
member.setId("member1");  
member.setUsername("회원1");

// 엔티티를 영속 ➜ 1차 캐시에 저장된다 
em.persist(member);

// 1차 캐시에서 조회한다
Member findMember = em.find(Member.class, "member1");

객체를 영속성 컨텍스트에 저장한 후에 조회를 했을 때 JPA 는 DB 를 찾아보는 것이 아니라 먼저 1차 캐시를 찾아보게 됩니다. 1차 캐시에 존재한다면 1차 캐시에 있는 엔티티를 가져오게 됩니다.

[ 1차 캐시에 존재하지 않는 경우 ]

만약 조회를 했는데 1차 캐시에 없다면 DB 에서 조회를 합니다. 그 후 조회 결과를 1차 캐시에 저장하고 조회한 엔티티를 반환합니다. 이후에 또 조회를 한다면 1차 캐시에 있는 엔티티가 조회되므로 쿼리가 최초 1번만 실행되게 됩니다.


3-3. 영속 엔티티의 동일성 보장

Member a = em.find(Member.class, "member1");  
Member b = em.find(Member.class, "member1");

System.out.println(a == b); // 동일성 비교 true

자바 컬렉션에서 동일한 것을 조회하면 동일한 인스턴스로 판단됩니다. 이처럼 JPA 도 영속 엔티티의 동일성을 보장해줍니다.

이것이 가능한 이유는 1차 캐시입니다. 영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티 인스턴스를 반환하기 때문에 동일성을 보장하는 것입니다.


3-4. 1차 캐시와 트랜잭션

엔티티 매니저는 DB 트랜잭션 단위로 만들고. 트랜잭션이 끝날 때 함께 종료하게 됩니다. 즉, 고객의 요청이 하나 들어와서 비즈니스가 끝나버리면 영속성 컨텍스트를 지운다는 의미입니다. 그렇게 되면 1차 캐시도 함께 날라가게 됩니다.

그렇기 때문에 1차 캐시는 여러 명의 고객이 사용하는 캐시가 아니고, 한 트랜잭션 안에서만 효과가 있으므로 정말 찰나의 순간에서만 이득이 있기 때문에 큰 이점이 있진 않습니다. 성능적인 이점은 없지만 이렇게 동작한다는 컨셉에서 오는 이점은 분명히 존재합니다.

참고로 애플리케이션 전체에서 공유하는 캐시는 JPA 나 Hibernate 에서 2차 캐시라는 것이 존재합니다.




4. 트랜잭션을 지원하는 쓰기 지연

transaction.begin();  // [트랜잭션] 시작

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//-- 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

// 커밋하는 순간 데이터베이스에 INSERT SQL 을 모아서 보낸다.
transaction.commit(); // [트랜잭션] 커밋

트랜잭션이 시작되고 persist() 를 통해 영속성 컨텍스트에 엔티티를 저장합니다. 저장되면 JPA 가 엔티티를 분석해서 Insert 쿼리를 생성하고, 쓰기 지연 SQL 저장소에 쌓아둡니다.

트랜잭션이 커밋되면 SQL 저장소에 쌓여있던 Insert 쿼리들이 flush 가 되면서 날라가게 되고, 그 후에 트랜잭션이 커밋되는데 이를 쓰기 지연이라고 합니다.

hibernate.jdbc.batch_size 옵션이 있는데 여기서 지정된 수만큼 모았다가 DB 에 쿼리를 날리고 커밋합니다.




5. 변경 감지( Dirty Checking )

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();  // [트랜잭션] 시작

// 영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");

// 영속 엔티티 데이터 수정
memberA.setUsername("hi");
memberA.setAge(10);

// em.update(member) 이런 코드가 있어야 하지 않을까?

transaction.commit(); // [트랜잭션] 커밋

엔티티를 수정한 후에는 DB 에 반영하기 위해 em.update() 와 같은 코드가 필요할 것 같은데 해당 코드 없이 엔티티를 수정하고 커밋하면 DB 의 데이터가 수정됩니다. 이것이 가능한 이유는 JPA 의 변경감지 기능 때문입니다.

  1. 값을 읽어온 최초 시점의 상태, 즉 영속성 컨텍스트의 1차 캐시에 들어온 상태를 스냅샷을 뜨게 됩니다.

  2. JPA 는 트랜잭션을 커밋하는 시점에 내부적으로 flush() 라는게 호출되면서 엔티티와 스냅샷을 비교합니다.

  3. 비교했을 때 엔티티가 변경되었다면 업데이트 쿼리를 쓰기 지연 저장소에 만들어두게 됩니다.

  4. 그 후 flush 를 통해 DB 에 반영을 하고, 트랜잭션을 커밋하게 됩니다.




6. 플러시

6-1. 플러시

플러시는 트랜잭션이 커밋될 때 일어나게 되는데 영속성 컨텍스트의 변경 내용을 DB 에 반영합니다. Insert 쿼리나 update 쿼리가 DB 에 날라가는 것이라고 생각하면 됩니다.

플러시가 발생하면 먼저 변경 감지가 일어나고, 수정된 엔티티 쓰기 지연 SQL 저장소에 등록합니다. 그 후 쓰기 지연 저장소의 쿼리를 DB에 전송합니다.

플러시가 발생한다고 트랜잭션이 커밋되는 것이 아닌 플러시를 통해 DB 에 쿼리 저장소에 있는 쿼리들을 전송한 후에 커밋이 일어나게 됩니다. DB 에 쿼리 저장소에 있던 쿼리들이 날라가도 커밋하지 않으면 저장되지 않습니다.

참고로 플러시를 한다고 해서 1차 캐시가 비워지진 않습니다.


6-2. 영속성 컨텍스트를 플러시 하는 방법

  1. em.flush() ➜ 직접 호출 : 엔티티 매니저의 flush() 메서드를 직접 호출해서 영속성 컨텍스트를 강제로 플러시합니다.

  2. 트랜잭션 커밋 ➜ 플러시 자동 호출

  3. JPQL 쿼리 실행 ➜ 플러시 자동 호출

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// -- 이 시점에 DB 에 Insert 쿼리가 날라간 상태가 아님

 // 중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members= query.getResultList();

em.persist() 를 했을 때 DB 에 쿼리가 날라가지 않습니다. 그 후 JPQL 을 통해 member 에 대한 조회가 이루어질 때 DB 에 저장된 것이 없기 때문에 아무것도 조회되지 않습니다.

그래서 JPA 는 이러한 것을 방지하고자 기본 모드가 JPQL 을 실행할 때는 무조건 플러시를 날리게 되어 있습니다. 결과적으로 위의 코드는 정상적으로 조회가 이루어집니다.


6-3. flush() 와 flush

em.flush() 를 하면 아래 3개의 동작이 수행되며, 그 중 3번째에 있는 것이 flush 입니다. 그림에서도 flush() 와 flush 를 구분하고 있습니다.

  1. 엔티티와 스냅샷 비교 후 변경된 것에 대한 SQL 생성 ( 변경 감지 )

  2. 생성된 SQL을 쓰기 지연 SQL 저장소에 등록

  3. 쓰기 지연 SQL 저장소에 있는 쿼리를 DB에 반영하는 행위 ( flush )


6-4. 플러시 모드 옵션

em.setFlushMode() 를 통해 플러시 모드 옵션을 지정할 수 있습니다.

  • FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러시 (기본값)

  • FlushModeType.COMMIT : 커밋할 때만 플러시


6-5. 정리

  • 플러시는 영속성 컨텍스트를 비우지 않습니다.

  • 플러시는 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화 합니다.

  • 플러시는 트랜잭션이라는 작업 단위가 중요한데 커밋 직전에만 변경 내용을 DB 에 날려 동기화 하면 됩니다.




7. 준영속 상태

7-1. 준영속 상태

1차 캐시에 올라가 있는 상태를 영속 상태라고 합니다. 그래서 영속 상태가 되는 2가지 방법은 persist() 를 호출하거나 find() 를 호출하는 것입니다.

준영속 상태란 영속 상태의 엔티티가 영속성 컨텍스트에서 분리되는( 빠지는 ) 것을 의미합니다. 그렇기 때문에 변경 감지와 같은 기능을 사용할 수 없게 됩니다.

영속 상태를 준영속 상태를 만드는 방법은 3가지가 있는데 하나씩 살펴보도록 하겠습니다.


7-2. detach

Member member = em.find(Member.class. 1L);  // 영속 상태
member.setName("memberA");  // 엔티티 변경

em.detach(member);  // 준영속 상태로 변경 ➜ JPA 가 관리하지 않는다

tx.commit();  // 아무일도 일어나지 않는다

em.detach(entity) 는 특정 엔티티만 준영속 상태로 전환하는 것을 의미합니다.

위의 예시에서 find() 를 통해 Member 를 가져옵니다. 이때는 영속 상태를 유지합니다. 하지만 name 을 변경한 후 detach() 를 통해 준영속 상태로 변경하게 되면 JPA 가 관리하지 않게 됩니다.

그렇기 때문에 트랜잭션을 커밋할 때 변경 감지가 일어나지 않아 아무일도 일어나지 않게 됩니다.


7-2. clear

Member member = em.find(Member.class. 1L);  // 영속 상태

em.clear(); // 영속성 컨텍스트를 통채로 지운다

Member member = em.find(Member.class. 1L);  // 영속 상태

em.clear() 는 영속성 컨텍스트를 완전히 초기화 하는 것을 의미합니다.

위의 예시에서 조회 쿼리는 2번 나가게 됩니다. 처음 조회할 때 쿼리가 실행되고, 영속성 컨텍스트를 초기화한 뒤 다시 조회하면 또 쿼리가 실행됩니다. 당연히 영속성 컨텍스트를 초기화했기 때문입니다.

만약 처음 찾은 엔티티를 em.clear() 후에 값을 변경해도 변경 감지가 일어나지 않습니다.


7-4. close

em.close() 는 영속성 컨텍스트를 종료하는 것을 의미합니다. 영속성 컨텍스트를 종료하면 해당 영속성 컨텍스트가 관리하던 영속 상태의 엔티티가 모두 준영속 상태가 됩니다.

profile
공부한 내용을 정리해서 기록하고 다시 보기 위한 공간

0개의 댓글