본문 바로가기

Spring 정리/Spring JPA

JPA 영속성 컨텍스트

반응형

개요


 일반적으로 Spring Data Jpa를 사용하여 간단한 CRUD를 작업할 수 있습니다. 쉽게 사용 가능한 이유는, 해당 메서드들이 인터페이스에 추상화되어 있어 개발자는 가져다가 사용만 하면 되기 때문입니다. 한 단계 깊이 들어가서, 해당 인터페이스를 구현한 SimpleJpaRepository를 보면, Entity Manager를 통해 각 메서드들을 직접 구현했음을 확인 할 수 있습니다. 해당 기능으로 제어하는 JPA의 중요한 개념인 영속성의 동작 원리를 알아보겠습니다.

 

 

영속성 컨텍스트란?


 영속성 컨텍스트는 "엔티티를 영구 저장하는 환경"이라는 뜻입니다. EntityMnager.persist(entity); 가 대표적인 코드입니다. 영속성 컨텍스트는 논리적인 개념으로, 눈에 보이지 않지만, Entity Manager를 통해 영속성 컨테스트에 접근할 수 있습니다.

 

기본적으로, 영속성 컨텍스트에 있는 내용이 실제 DB에 반영되는 때는 트랜잭션이 commit될 때입니다.

 

 

 

영속성의 4가지 상태


비영속(new/trasient)

- 영속성 컨텍스트와 전혀 관계없는 상태

 

만약 새로운 엔티티가 만들졌다면, 어떠한 영속성 컨텍스트에도 속하지 않습니다.

member는 영속 컨텍스트 밖에 있다.

 

 

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

 

Member 엔티티가 생성되고, Id와 Username이 설정 되었을 뿐, 영속성과는 아무런 연관이 없습니다.

 

 

 

영속(managed)

 

- 영속성 컨텍스트에서 관리되는 상태

 

DB에서 조회하거나, persist()로 엔티티가 영속성 컨텍스트에 저장되어 있는 상태입니다.

member는 영속 컨텍스트 안에 있다.

 

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

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

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

 

em.persist(member);로 member를 Entity Manger인 영속성 컨텍스트에 추가하였습니다.

 

 

준영속(detached)

- 영속성 컨텍스트에 저장되어있었지만 이후에 분리된 상태

 

//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);

 

em.detach(member)를 통해, 영속성 컨텍스트에 있던 member를 다시 밖으로 분리해냅니다.

 

 

삭제(removed)

- 삭제된 상태

 

//객체를 삭제한 상태(삭제)
em.remove(member);

 

em.remove(member)를 통해, 객체 자체를 삭제하도록 합니다. 트랜잭션이 완료되면, 삭제쿼리가 실행되어, member가 삭제됩니다.

 

엔티티 생명주기

 

 

영속성 컨텍스트의 이점


1.  1차 캐시 저장, 조회

 

영속성 컨텍스트로 persist = 1차 캐시 저장

 

 

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

 

em.persist(member)로 영속성 컨텍스트 안에 member를 넣습니다. 사진에서 보다시피 1차캐시에 @IdEntity가 저장됩니다. 현재, 이 상태가 영속성 컨텍스트 안에서 1차 캐시에 저장된 상태입니다. 하지만 persist()를 한다고 하는 것이 DB에 저장되는 것은 아니다!

 

1차 캐시는, 트랜잭션 단위로 작동합니다. 즉, 10명의 사용자가 조회 기능을 사용한다면, 1차 캐시를 공유하는 것이 아니라 각 사용자의 트랜잭션 단위로 총 10개의 Entity Manager가 생성됩니다. 따라서, 1차 캐시의 성능적인 이점이 크다고는 할 수 없습니다.

 

1차 캐시에서 조회

 

 

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

//1차 캐시에 저장됨
em.persist(member);

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

 

em.persist(member)로 영속성 컨텍스트에 member를 넣으면 1차 캐시에 저장됩니다. 영속성 컨텍스트에서 find()로 조회하면, 무조건 1차 캐시에 해당 객체가 있는지 없는지 검사를 먼저 합니다. 1차 캐시에 있다면 바로 꺼내오기 때문에 별도의 DB 쿼리문이 발생하지 않는다.

 

1차 캐시에 없는 객체는 DB에서 불러오고 캐시에 저장

 

Member findMember2 = em.find(Member.class, "member2");

 

만약 1차 캐시에 없는 객체를 find()로 호출하려고 한다면?

 

=> DB에 쿼리를 날려서 받아온 값으로 응답합니다.. 이 때, 1차 캐시에 값이 없었기 때문에 DB에서 얻은 객체를 1차 캐시에 저장합니다.

 

 Entity Manager의 생명주기는 transaction과 같아서 보통 transaction이 끝나면 영속성 컨텍스트가 삭제됩니다. 다시말해, transaction이 끝나면 EntityManager의 생명주기도 끝납니다. 즉, 1차 캐시에 저장해 두었던 모든 캐시 데이터도 날라갑니다. 따라서 transaction 범위 내에서만 1차 캐시가 유효하며, 실시간 대용량을 처리하는 경우, 실시간으로 반복된 요청이 엄청나게 발생하므로 1차 캐시의 효율이 올라갑니다.

 


2. 동일성 보장(identity)

 

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

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

 

영속성 컨텍스트 안에 존재하는 객체는 동일성을 보장합니다.

 


3.  트랜직션을 지원하는 쓰기 지연

 

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();

//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

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

 

  다시 반복하지만, Entity Manager의 em.persist(memberA); em.persist(memberB);는 영속성 컨텍스트에 넣을 뿐, DB에 반영하는 것이 아닙니다.. transaction.commit();을 해야만 DB에 쿼리를 날립니다. 반대로 말해, transaction.commit()을 하지 않으면 고의적으로 DB에 반영되는 시기를 늦출 수 있습니다.

 

 이제 새로운 개념이 나오는데, 쓰기 지연 SQL 저장소입니다. em.persist(memberA);, em.persist(memberB); 를 통해 1차 캐시에 저장이 되면, INSERT SQL이 생성되어 쓰기 지연 SQL 저장소에 저장됩니다. 여기에 SQL문을 보관하고 있다가 transaction.commit();을 하고 나서야 비로소 모든 INSERT SQL이 DB에 한번에 반영됩니다.

 

영속성 컨텍스트에 persist()하면, 쓰기 지연 저장소에 쿼리문이 추가된다

 

 

transaction.commit()을 해야 비로소 DB에 저장

 

 persist()는 단순히 영속성 컨텍스트에 추가하는 것이고, 실제로는 transaction.commit()을 한 순간, 쓰기 지연 SQL 저장소에 있던 쿼리들이 한번에 DB로 저장됩니다.(JPA 관련 용어로 flush한다고 표현한다) 이 때, 지연쓰기의 사이즈를 정할 수 있어, 기준 갯수가 만족되어야 DB에 반영됩니다.

 


4. 변경 감지(dirty check)

 

 EntityManger 인터페이스의 메서드에는 조회를 위한 find, 저장을 위한 persist, 삭제를 위한 remove가 있지만 수정과 관련한 것은 없습니다. 왜 update 관련한 메서드가 존재하지 않을까요? 결론은, 영속성 컨텍스트에서 스냅샷을 이용해서 새롭게 persist되는 내용과 비교하여 수정하기 때문입니다.

 

변경 감지를 담당하는 스냅샷

 

 트랜잭션이 커밋되어 flush()가 동작할 때, 영속성 컨텍스트에 있는 현재 캐시값과, 기존에 저장되어 있던 스냅샷을 비교해 차이가 있는지 확인합니다. 달라진 부분이 있다면, 최신화를 위해서 Update SQL을 생성합니다. 따라서 단순히 영속성 컨텍스트에서 조회한 객체의 필드를 변경해주기만 해도 자동으로 영속성 컨텍스트에서 처리합니다.

 

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(); // [트랜잭션] 커밋

 

update를 하나도 사용하지 않고 영속성 컨텍스트에서 호출한 memberA의 필드를 수정만하면  됩니다. memberA.setUsername("hi"); memberA.setAge(10); 로 각가 이름과 나이를 변경하고 transaction.commit()을 하면, 영속성 컨텍스트의 1차 캐시에 있는 memberA의 스냅샷과 변경 내용을 비교해, 새로운 Update SQL을 생성하여 반영합니다.

 

 

5. 엔티티 삭제

 

//삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, “memberA");

em.remove(memberA); //엔티티 삭제

 

em.remove()를 통해, 영속성 컨텍스트에 있는 엔티티를 삭제 할 수 있습니다. 쓰기 지연 SQL 저장소에는 delete SQL문이 생성됩니다.

 

 

-  준영속 상태 만들기

 

기존에는 영속이었으나, 준영속 상태로 변화된 것을 의미합니다.

따라서 준영속이 되면, 영속 상태일 때의 기능을 사용하지 못합니다.

 

em.detach(entity)
특정 엔티티만 준영속 상태로 전환

em.clear()
영속성 컨텍스트를 완전히 초기화

em.close()
영속성 컨텍스트를 종료

 

 

플러시(flush)란?


 플러시는 영속성 컨텍스트에서 변경 감지를 하여 SQL 저장소에 쿼리를 등록하고, SQL들을 DB에 반영하는 것입니다. 지금까지는 계속 transaction.commit();의 방법으로 실제 DB를 반영하는 방법을 알아보았는데, 이외에 몇가지 방법이 더 있습니다.

 

1. em.flush() - 직접 호출

2. transaction.commit() - 플러시 자동 호출

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

 

 

1,2번은 위에서 계속 확인했기 때문에 3번을 살펴봅니다.

 

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);

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

 

 em.createQuery로 JPQL 쿼리를 호출하는 순간, 영속성 컨텍스트에 모든 내용이 반영되었다고 인식이 되어, 자동으로 플러시를 호출합니다. 따라서, memberA, memberB, memberC가 모두 DB에 저장됩니다. 혹여나, JPQL 쿼리를 실행할 때 자동으로 플러시가 호출되는 것을 막고 싶다면 FlushModeType.COMMIT으로 커밋히에만 호출하도록 설정을 바꿀 수 있습니다.

 

플러시를 한다고해서, 영속성 컨텍스트를 비우는 것은 아닙니다. 계속 캐시가 남아있습니다. 플러시의 가장 중요한 역할은 DB에 SQL을 반영하는 것입니다.

 

 

정리


 

JPA의 영속성 컨텍스트의 개념과 JPA 동작원리, 사용법을 살펴보았습니다.

 

영속성 컨텍스트는 비영속, 영속, 준영속, 삭제 4가지 상태를 가집니다.

영속성 컨텍스트는 트랜잭션 주기와 일치하므로, 트랜잭션 commit 기준으로 영속성 컨텍스트를 관리해야 합니다.

flush()를 통해,  트랜잭션이 commit되기 전에, DB에 강제 반영이 가능합니다.

flush()를 해도 영속성 컨텍스트에 데이터는 남아있고, 트랜잭션이 끝나면 영속성 컨텍스트도 제거됩니다.

엔티티를 수정하는 경우, update를 따로 하지 않아도 변경 감지를 이용해 자동으로 update를 합니다.

flush() -> 1차 캐시 스냅샷 비교 -> 쓰기 지연 SQL 저장소에서 SQL 생성 -> DB 반영의 순서로 작업합니다.

 

 

* 참고

자바 ORM 표준 JPA 프로그래밍 - 기본편

반응형

'Spring 정리 > Spring JPA' 카테고리의 다른 글

N:1 테이블 관계 설계하기  (0) 2021.09.07
1:N 테이블 관계 설계하기  (0) 2021.09.07
1:1 테이블 관계 설계하기  (0) 2021.09.07
Entity  (2) 2021.09.05
JPA  (0) 2021.08.28