ORM 표준 JPA 프로그래밍 - 영속성 관리

링딩·2022년 8월 27일
0

ORM 표준 JPA

목록 보기
2/6

김영한 강사님의 해당 강의를 통해 해당 글을 작성하였습니다.

들어가기 전에

JPA에서 가장 중요한 2가지가 있다.

  • 객체와 관계형 데이터베이스 매핑하기
    (Object Relational Mapping)
  • 영속성 컨텍스트

이렇게가 있다. 우리는 이들 중 '영속성 컨텍스트'에 대해서 알아보자.


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

  • 'EntityManagerFactory'를 통해 고객의 요청이 올 때마다 'Entity Manger'를 생성해준다.
  • 이 'EntityManager'은 내부적으로 '커넥션 풀'을 이용하여 DB를 사용한다.

영속성 컨텍스트

  • EntityManager를 통해서 '영속성 컨텍스트'에 접근할 수 있다.
    -> 눈에 보이지 않는 어떤 공간이 생긴다 라고 생각하면 된다.
    +) 엔티티 매니저와 영속성 컨텍스트가 N:1인 것도 있는데 이것은 뒤에서 다루겠다.
  • JPA를 이해하기 위해 가장 중요한 용어이다.
  • EntityManger.persist(entity)
    => 엔티티를 '영속성 컨텍스트'에 저장하겠다.

엔티티의 생명주기

크게 이렇게 4가지로 나뉘게 된다.

• 비영속 (new/transient)
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
• 영속 (managed)
영속성 컨텍스트에 관리되는 상태
• 준영속 (detached)
영속성 컨텍스트에 저장되었다가 분리된 상태
• 삭제 (removed)
삭제된 상태

이 생명주기에 대해서 좀 더 자세히 알아보자!! 🤔


<비영속>

//객체를 생성한 상태(비영속) 
Member member = new Member(); 
member.setId("member1"); 
member.setUsername("회원1");
  • 정말 객체를 생성하고 이에 대해 세팅만 한 상태를 말한다. =>'영속성 컨텍스트'에 들어가지 x
    => JPA에 관계없음.

<영속>

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

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

//객체를 저장한 상태(영속) => '컨텍스트'에 담김 => 1차캐시에
em.persist(member);
  • persist() 를 해준다고 DB에 바로 저장되는 것이 x
    -> '영속성 컨텍스트'에 있는 1차 캐시에 저장됨
  • DB에 언제 저장되냐? commit() 때 저장된다.
    -> '영속성 컨텍스트'에서 DB로 쿼리가 날아간다.

<준영속>

웹 애플리케이션 제작에서 더 자세히 다뤄볼 것이지만 그래도 개념을 한 번씩 보고가보자!

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

앞에서도 말했지만 영속의 경우는 persist(), em.find()나 jpa를 통해서 조회를 했으나 '영속성 컨텍스트'에 없어서 DB에서 꺼내와서 '영속성 컨텍스트'에 올리는 것도 => 영속!

  • 영속 상태의 엔티티가 -> '영속성 컨텍스트'에서 분리 되는 것
  • 영속성 컨텍스트가 제공하는 기능을 사용 (x)
    ex 더티체킹

준영속 상태로 만드는 3가지 방법

em.detach(entity)
- 특정 엔티티만 준영속 상태로 전환
- JPA에서 관리하지 않겠다는 뜻이다.
- 이 직후 commit() 하면 아무 일도 일어나지 않는다.
ㄴ> JPA가 관리하지 않겠다고 한거라...

em.clear()

  • 영속성 컨텍스트를 완전히 초기화
  • 이 후에 다시 조회한다면?
    -> 완전히 초기화 되었기에 '1차 캐시가' 없다.
    => 초기화 된 상태이므로 쿼리가 2번 올라온다(다시 세팅)

em.close()

  • 영속성 컨텍스트를 종료
    => 아예 볼 수 없다.

<삭제>

//객체를 삭제한 상태(삭제) 
em.remove(member);
  • 실제 DB에서 지워달라는 뜻이다.




영속성 컨텍스트의 이점

• 1차 캐시
• 동일성(identity) 보장
• 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
• 변경 감지(Dirty Checking)
• 지연 로딩(Lazy Loading)



1차 캐시

여기서 말하는 1차 캐시를 영속성 컨텍스트와 비슷하게 생각해도 좋다.

em.persist() 라는 것은 1차 캐시에 저장을 하겠다는 뜻인데

조회에서의 1차 캐시

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

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

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

어떻게 생겼을지 그림으로 직접 보자.

여기서 key가 PK, 값은 Entity가 되는데 '조회'를 시도했을 때 가장 먼저 1차 캐시에서 해당 객체를 찾는다.(물론 DB에 존재한다)


만약 1차 캐시에서 바로 조회되지 않는다면?

예를 들어 앞의 코드에서 member1을 조회했고 그 후 member2를 조회하라고 한다면...

  1. DB에서 조회하고(DB에 저장되어 있어야 함)
  2. 1차 캐시에다 가져와서 저장시킴
  3. 1차캐시 (현재로 이해하면 영속성 컨텍스트)에 저장된거를 반환
    => 그 이후에 얘를 다시 찾는다고 하면, 바로 1차캐시(영속성 컨텍스트)에서 찾을 수 있는거지

+) 참고

사실상 큰 도움은 안된다고 한다. 영속성 컨텍스트(EntityManager)은 데이터 베이스 트랜잭션 단위로 이루어져 있어서, 이것이 끝날 때 영속성 컨텍스트도 다 날라간다.
찰나의 순간에만 이득이 있기 때문에 모두와 공유하는 것이 아니기도 하고 성능적인 이점이 그렇게 크진 않다.


영속 엔티티의 동일성 보장

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

System.out.println(a == b); //동일성 비교 tru
  • JPA가 영속 엔티티의 동일성 보장
  • 마치 자바 컬렉션에서 똑같은 레퍼런스가 있는 것을 꺼낸 것처럼 => 1차캐시가 있기 때문에 가능하다.
  • 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공
    => 조금 어려운 내용이라 '같은 트랜잭션 내'에서 비교하면 아 레퍼런스 변수니까 TRUE가 됬구나 이렇게 생각하자.

엔티티를 등록할 때 트랜잭션을 지원하는 쓰기 지연이 가능하다!

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작

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

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

  1. 1차 캐시에 들어감
  2. JPA가 얘를 분석해서 INSERT SQL을 생성해서 '쓰기 지연 SQL 저장소'에 미리 저장시켜 쌓아놓음
  3. commit() 하게 되면, flush()되어서 얘네들이 DB에 한 번에 날라감

[여기서!!! 잊지마시오] 🧨

JPA는 내부적으로 리프렉션 등이 쓰이기 때문에 동적으로 객체를 생성해야 한다.
결론: '기본 생성자'가 있어야 한다.
-> 꼭 public 일 필요 (x)


엔티티 수정 - 변경감지(더티체킹)

들어가기 전 우리는 버퍼링이라는 개념에 대해 알아야 한다.

  • 버퍼링
    - 모았다가 DB에 한 번에 넣는 것을 버퍼링이라 함
    - (이점) 쌓아서 모았다가 보내는게 좋다
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(); // [트랜잭션] 커밋

이 코드의 결과는 엔티티 데이터가 수정된 상태가 반영되어 조회가 된다

왜일까? 따로 수정하는 코드가 필요하지 않을까?
정답은 아니다!! 그림을 통해 이해해보자

영속성 컨텍스트에 비밀이 있다! 🤔

commit을 하면 내부적으로 flush()가 일어난다.

  1. 1차 캐시에선 '스냅샷'이란게 있다
    ∴ '스냅샷' 이란건 값을 DB에서 처음 읽어와서 집어넣었을 때건, 딱 읽어온 최초 시점을 찍어놓은 것
  2. JPAEntity와 스냅샷을 비교
  3. 이 둘이 비교했는데.. 바꼈다면?
    -> '쓰기 지연 SQL 저장소'에 UPDATE 쿼리를 생성해 만들어 둔다 => 변경감지
  4. 이것을 DB에 반영 -> commit 해준다.

지연 로딩(LAZY)

간단히 말하자면 즉시 로딩은 데이터를 조회할 때 연관된 데이터까지 한 번에 불러오는 것이고, 지연 로딩필요한 시점에 연관된 데이터를 불러오는 것이라고 할 수 있다.

지연 로딩을 사용하면 관련된 SQL문을 한 번에 불러오는 것이 아닙니다.

Q. 왜 지연 로딩을 써야 하는가?

A. 테이블 설계가 복잡해질수록, 하나의 엔티티가 참조하는 테이블들은 늘어날 테고, 그에 따른 쿼리문도 굉장히 길어지겠죠. 이런 복잡한 쿼리문을 본 개발자는 해당 도메인이 어떻게 설계되었는지 확인해보아야 하고, 논리적인 Layer 분리가 이뤄지지 않은 셈이 되죠. 이러한 부분은 유지 보수를 힘들게 만들 수 있습니다.
또한 시간이 지나면 변화되고 추가가 되기 마련이기에 '성능 이슈'가 발생할 가능성을 줄이고자 즉시로딩 보다는 지연로딩을 더 권장합니다.

+) fetch의 디폴트 값은 @xxToOne에서는 EAGER, @xxToMany에서는 LAZY이다.


엔티티 삭제

  • commit 시점에 DELETE 쿼리가 나간다.
//삭제 대상 엔티티 조회 
Member memberA = em.find(Member.class, “memberA");
em.remove(memberA); //엔티티 삭제

플러시

  • '영속성 컨텍스트'의 변경내용을 DB에 반영
  • 쉽게 말해 '영속성 컨텍스트'의 변경사항과 데이터베이스의 상황을 맞추는 것 => 쿼리를 날려주는 것
  • commit()이 일어나면 flush()가 발생
  • 1차 캐시가 지워지지 x
    -> 오직 '쓰기 지연 SQL 저장소', '변경감지'가 된게 DB에 반영되는 것

플러시 발생시 무슨 일이?

  • 변경감지 발생
  • 수정된 엔티티를 '쓰기 지연 SQL 저장소'에 등록한다
  • 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제 쿼리)

영속성 컨텍스트를 플러시하는 방법

em.flush() - 직접 호출
트랜잭션 commit() - 플러시 자동 호출
JPQL 쿼리 실행 - 플러시 자동 호출
-> DB에 쿼리로 라도 날라가야 하는데 아예 날라가지 않기 떄문에 자동으로 호출해주어야 한다.


+) 플러시 모드 옵션

em.setFlushMode(FlushModeType.COMMIT)

FlushModeType.AUTO
:커밋이나 쿼리를 실행할 때 플러시 (기본값)
-> 가급적 손 대지 말고 이거로 쓰자...
FlushModeType.COMMIT
: 커밋할 때만 플러시

최종 정리...

• 영속성 컨텍스트를 비우지 않음
• 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화
• 트랜잭션이라는 작업 단위가 중요
-> 어쨌든 commit 직전에만 변경사항을 동기화 해주면 됨

profile
초짜 백엔드 개린이

0개의 댓글