영속성 관리

뚝딱이·2022년 8월 27일
0

JPA

목록 보기
2/11

JPA에서 가장 중요한 2가지는 아래와 같다.

  • 객체와 관계형 데이터베이스 매핑하기 (설계와 관련된 부분- 객체를 어떻게 설계하고 데이터베이스를 어떻게 설계해서 그 둘을 어떻게 연결, 매핑할 것인지)
    정적이다.
  • 영속성 컨텍스트 : 실제 JPA가 내부에서 어떻게 동작해하는 부분
    영속성 컨텍스트를 이해하면 JPA가 내부적으로 어떻게 동작하는지 알게된다.

엔티티 매니저 팩토리와 엔티티매니저

엔티티 매니저 팩토리를 통해 고객의 요청이 올때 마다 엔티티매니저를 생성한다. 엔티티매니저는 내부적으로 DB CONNECTION을 사용해서 DB를 사용한다.

영속성 컨텍스트

JPA를 이해하는데 가장 중요한 용어이다.
영속성 컨텍스트를 해석해보자면, 엔티티를 영구 저장하는 환경이라는 뜻이다.
EntityManager.persist(entity) -> entity를 저장한다는 것일까 ? 아니다. 사실 db에 저장한다는게 아니라 사실은 영속성 컨테스트를 통해 entity를 영속화 한다는 것이다.
더 정확히 말하면 persist ->entity를 DB에 저장하는게 아니라 영속성 컨텍스트에 저장하는 것이다.

엔티티매니저 ? 영속성컨텍스트 ?

영속성 컨텍스는 논리적인 개념으로 눈에 보이지 않음
엔티티매니저를 통해 영속성 텍스트에 접근

J2SE환경

엔티티매니저와 영속성 컨텍스가 1:1
엔티티매니저를 생성하면 그 안에 1:1로 영속성 컨텍스가 생성됨
눈에 보이지 않는 공간이 생긴다.

엔티티의 생명주기

비영속 (new/transient)

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

최초의 멤버객체를 생성한상태로 EntityManager.persist하면 영속상태가 된다.
MEMBER객체 생성하고 엔티티 매니저에 아무것도 안넣은 상태이다.
객체를 세팅만 한 상태로 JPA와 전혀 관계가 없는 상태 이기 때문에 비영속 상태라 한다.

영속 (managed)

영속성 컨텍스트에 관리되는 상태이다.

//객체를 생성한 상태(비영속) 
Member member = new Member(); 
member.setId("member1"); 
member.setUsername(“회원1);
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//객체를 저장한 상태(영속)
em.persist(member);

엔티티 매니저안에는 영속성 텍스트가 있다.
MEMBER객채 생성뒤에 엔티티 매니저를 얻어와서 객체를 엔티티 매니저에 persist를 사용해서 넣으면 영속성 텍스트에 MEMBER가 들어가면서 영속상태가 된다.

Member member = new Member();
member.setId();
member.setName()
--여기 까진 비영속

em.persist(member) 
--여기 부터 영속

persist앞뒤로 로그를 찍어보면, 즉

System.out.println("--befor--")
em.persist(member)
System.out.println("--after--")

before와 after사이에 sql 쿼리를 날릴 것 같지만 실제 쿼리는 after 이후에 날아간다.
따라서 영속상태가 된다고 해서 db에 쿼리가 바로 날아가는 것이 아니다.

언제 날아가냐? 트랜잭션이 커밋되는 순간에 날아간다.

준영속 (detached)

영속성 컨텍스트에 저장되었다가 분리된 상태

em.persist(member)
em.detach(member)

라고 detach를 사용하면 영텍에서 분리해서 준영속 상태가 된다.

삭제 (removed)

삭제된 상태

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

영속성 컨텍스트의 이점

영속성 컨텍스트는 웹 애플리케이션과 db사이의 중간계층이다.
중간 계층으로서 얻는 이점 : 버퍼링, 캐싱 등

엔티티 조회, 1차 캐시

영속성 컨텍스트는 내부에 1차 캐시를 들고 있다.

사실상 em을 영속성 컨텍스트로 이해해도 된다. 다만 약간의 차이는있다.
1차 캐시를 영속성 컨텍스트로 이해해도 된다.

Member member = new Member();
member.setId();
member.setName()
--여기 까진 비영속

em.persist(member)

까지 하면 영속성 컨텍스트의 1차 캐시에 저장한다.
1차 캐시는 MAP으로 되어있는데 KEY값이 ID가 되고 VALUE가 ENTITY가 된다. ENTITY는 객체 자체의 값이다.
따라서 위에선 MEMBER가 ENTITY, MEMBERID가 ID이다.

왜 이런 구조를 사용하는가. 조회할 때를 보자.

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//1차 캐시에 저장됨
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");

Member findMember = em.find(Member.class, "member1");
먼저 1차 캐시를 뒤진다. 1차 캐시를 뒤졌을 때 있으면 db 조회안하고 캐시에서 가져온다.

Member findMember2 = em.find(Member.class, "member2");
member2를 조회했을 때 1차 캐시에 없다면 영속성 컨텍스트의 1차 캐시에 없음을 알아차리고 DB에 조회해 member2를 1차 캐시에 저장한다. 그 후에 member2를 반환한다.
따라서 이후에 조회하면 1차 캐시에서 조회가 되지만 사실상 큰 도움은 안된다.
엔티티 매니저는 db트랜잭션 단위로 만들고 종료될 때 없어진다. 즉 고객의 요청이 들어와서 비즈니스 로직이 끝나면 사라진다. 이때 1차 캐시도 사라진다. 그러니까 엄청 짧은 시간에 사용하는 것이다.
또한 여러 고객이 사용하는 것도 아니고 해당 고객의 요청시에만 db의 한 트랜잭션 안에서만 동작하기 때문에 성능에 별 도움 안된다. 그래도 비즈니스 로직이 진짜 엄청 복잡한 경우엔 도움된다.

em.persist(member)
Member findMember= em.find(member.class, 101L)

하고 출력하면 조회용 쿼리가 나가는가 ? select 쿼리가 안나감
persist에서 1차 캐시에 저장이 된것이다.
그래서 1차 캐시에서 가져오느라 쿼리가 안나가는 것이다.

Member findMember1= em.find(member.class, 101L)
Member findMember2= em.find(member.class, 101L)

1을 조회할 땐 쿼리가 나가지만 2를 조회할 땐 쿼리가 안나갈 것이다. 처음 조회때 1차 캐시에 저장될테니까

영속 엔티티의 동일성 보장

Member findMember1= em.find(member.class, 101L)
Member findMember2= em.find(member.class, 101L)

findMember1==findMember2를 했을 때 어떻게 되는가 true가 나온다.
자바 컬렉션에서 가져오는 것 처럼 jpa는 동일성을 보장해준다. 즉, == 비교를 보장해준다.
이게 가능한 이유는 1차 캐시가 있기 때문이다.

1차캐시로 반복가능한 읽기등급의 트랜잭션 격리 수준을 db가 아닌 애플리케이션 차원에서 제공

엔티티등록 트랜잭션을 지원하는 쓰기 지연

persist에선 sql을 db에 보내지 않고 commit하는 순간에 sql을 보낸다.

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋

라고 할때 동작 하는 법을 알아보자

em.persist(memberA)

먼저 1차 캐시에 들어간다.JPA가 ENTITY를 분석해서 INSERT 쿼리를 생성해서 쓰기 지연 SQL 저장소에 저장한다.

em.persist(memberB)
위와 똑같이 1차 캐시에 들어가고, INSERT쿼리를 생성해 지연 SQL저장소에 저장한다.

그럼 COMMIT 시점에 쓰기 지연 SQL 저장소에 있던 애들이 FLUSH가 되면서 날아간다. 그리고 실제 DB 트랜잭션이 커밋된다.

왜 굳이 ? 그냥 쿼리가 바로 나가면 안될까 ? 버퍼링 기능을 사용해야 되기 때문이다.

em.persist(memberA)
em.persist(memberB)

에서 persist할때 마다 쿼리를 날리면 최적화를 할 여지 조차 없다.

하이버네이트 옵션 중에 batch_size를 지정할 수 있다. 이 size를 지정하면 이 size만큼의 쿼리를 저장했다가 한번에 commit할 수 있다.

버퍼링 처럼 모았다가 보냄 -> 실무에선 실제로 많이 조회해서 사용할 일이 자주 없는데, 이러한 것을 잘 활용해서 성능을 먹고 들어갈 수 있다.

엔티티 수정 변경감지

jpa의 목적이 컬렉션 다루듯이 동작하는 것이기 때문에 컬렉션에선 값을 수정하고 다시 넣지 않는다. 따라서 jpa에서도 값을 찾아와 데이터를 변경하는 것 까지만 하고 넣어주는 건 하지 않아도 된다.

즉, set까지만 적으면 되는것이다.
jpa는 변경감지 기능 -> 영속성 컨텍스트 안에서 !
jpa는 커밋하는 시점에 flush가 된다. 엔티티와 스냅샷을 비교한다.
1차 캐시엔 스냅샷이란 게 있는데, 스냅샷을 값을 최초로 읽어온 시점, 최초로 영텍에 들어온 상태를 스냅샷을 떠놓는것이다.
jpa가 그러면 커밋되는 시점에 내부적으로 flush가 호출되면서 엔티티와 스냅샷을 비교하고 엔티티가 바뀌었으면 쓰기 지연 저장소에 update 쿼리를 만들어서 db에 반영한다.

if(member.getName().equals("ZZZ"){
em.update(member)
}

member가 변경된 경우에만 update 쿼리를 날림 이걸 안해도 jpa는 알아서 해줌

엔티티 삭제

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

플러시

영속성 컨텍스트의 변경내용을 db에 반영 커밋할 때 플러시가 일어나게 된다.
영속성 컨텍스트의 변경 사항과 db를 맞춰주는 것이다.

플러시 발생

  • 커밋되면 자동으로 발생함
  • 변경감지
  • 수정된 엔티티 쓰기 지연 sql 저장소에 등록
  • 쓰기지연 sql저장소의 쿼리를 db에 전송

플러시가 발생한다고 해서 트랜잭션이 커밋되는 것은 아니다.

영텍을 플러시하는 법

  • em.flush : 쓸일은 없는 데 나중에 테스트할때나 쓸 순 있으니 알아는 놔야한다.
  • 트랜잭션 커밋 : 플러시 자동 호출
  • jpql 쿼리 실행 : 플러시 자동 호출
em.persist(member)
em.flush

를 하면 flush의 메커니즘들이 즉시 실행된다.

-> 커밋 전에 sql 쿼리가 날아간다.

왜 jpql 쿼리를 실행하면 플러시가 자동 호출 되는가

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();

가 있다고 할 때, 아직 member들은 commit전이므로 db에 저장되지 않았다
하지만 아래의 jpql쿼리문으로 위에서 저장한 member들을 조회해야한다. 그래서 jpa는 문제를 방지하기 위해 jpql 실행전 항상 flush 실행한다.

플러시 모드 옵션

쓸일은 잘 없다.
em.setFlushMode(FlushModeType.COMMIT)
FlushModeType.AUTO : 지금 까지 배운것으로 기본값이다. 커밋이나 쿼리를 실행할 때 플러시한다.
FlushModeType.COMMIT : 쿼리 실행시엔 플러시 하지 않고 커밋할 때만 플러시 한다.

위의 상황이라고 할때 select로 member가 아닌 다른 것을 조회한다고 생각해보자. 그럼 flush할 필요가 없다.
가끔 쓸모있을 때가 있는데 사실 별로 도움 안되기 때문에 그냥 auto로 쓰는것이 좋다.

플러시는 영속성 컨텍스트를 비우는것이 아니다. 영속성 컨텍스트의 변경내용을 db에 동기화하는 것이다.
플러시의 메커니즘이 동작할 수 있는 이유는 트랜잭션이라는 작업단위가 있기 때문이다.

jpa는 데이터를 맞추거나 동시성에 대한 건 db 트랜잭션에 위임해서 사용한다.


준영속 상태

em.persist외에도 영속상태가 되는 경우 -> em.find를 했는데 만약 영텍에 없으면 1차 캐시에 올라가면서 영텍에 올라가게 된다.

준영속 상태란 영속상태의 엔티티가 영텍에서 분리되는것이다. 따라서 기능을 사용하지 못한다.

준영속 상태로 만드는 방법

  • em.detach(entity)
    특정 엔티티만 준영속 상태로 전환
  • em.clear()
    영속성 컨텍스트를 완전히 초기화
  • em.close()
    영속성 컨텍스트를 종료
Member member = em.find(Member.class, 100L)
member.setName("aaa")

em.detach(member)

하고 commit하게 되면 실제로 select 쿼리만 나가게 되고 update쿼리는 나가지 않는다.
왜일까. member가 detach에 의해 준영속상태가 되어 영속성 컨텍스트에서 분리되었으므로 엔티티 매니저는 member에 대해 신경 쓰지않아 관리하지 않는다.
따라서 dirty checking도 하지 않는다.

em.clear는 다 초기화한다.

Member member = em.find(Member.class, 100L)
member.setName("aaa")

em.clear()


Member member2 = em.find(Member.class, 100L)

하게 되면 select 쿼리 문이 2번 나간다. clear 때문에 1차 캐시가 다 초기화되었기 때문이다.


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

profile
백엔드 개발자 지망생

0개의 댓글