[JPA] 영속성 관리

Swim Lee·2021년 1월 26일
2

JPA

목록 보기
4/10
post-thumbnail

JPA에서 가장 중요한 2가지

  1. 객체와 관계형 데이터베이스 매핑 (Object Relational Mapping)

    • 설계와 관련된 부분
    • 정적인 내용
  2. 영속성 컨텍스트

    • 실제 JPA가 내부적으로 어떻게 동작하는가에 관련된 부분

영속성 컨텍스트

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

JPA 사용하기 위해선 먼저 엔티티 매니저 팩토리와 엔티티 매니저에 대해서 이해해야한다.

웹 애플리케이션이 실행될 때 엔티티 매니저가 같이 생성되고 (싱글톤?), 클라이언트의 요청이 올 때마다 엔티티 매니저 팩토리를 통해서 엔티티 매니저를 생성한다.

생성된 엔티티 매너저는 내부적으로 DB 커넥션을 사용해서 DB 접근한다.

영속성 컨텍스트

  • JPA를 이해하는데 가장 중요한 용어
  • "엔티티를 영구 저장하는 환경"이라는 뜻
  • EntityManger.persist(enitity);
    • 엔티티 객체를 DB에 저장하는 것이라고 알고있었음
    • 더 정확히 말하면, DB가 아니라 JPA의 영속성 컨텍스트에 저장하는 것임 (영속성 컨텍스트를 통해서 엔티티를 영속화한다고 함)

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

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

  • 스프링 프레임워크에서 EntityManager 주입받아서 사용하면, 같은 트랜잭션 범위에 있는 EntityManager는 동일 영속성 컨텍스트에 접근한다.
  • 따라서 동일한 @Transactional (같은 트랜잭션 범위 전파되는 경우에도) 이면 같은 영속성 컨텍스트에 접근한다.

엔티티의 생명주기

비영속 (new/transient)

영속성 컨텍스트와 전혀 관계가 없는 새로운 상태

영속 (managed)

영속성 컨텍스트에 관리되는 상태
em.persist(entity)

준영속 (detached)

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

삭제 (removed)

삭제된 상태

비영속 (new/transient)

//객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
  • 객체를 생성한 후 아직 영속성 컨텍스트에 저장되지 않은상태
  • 단순히 자바 객체를 생성하고 초기화만 해준 상태
  • JPA에 의해 관리되고 있지 않는 상태이다

영속 (managed)

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

//JPA 영속성 컨텍스트로의 접근은 엔티티 매니저를 통해서 한다
EntityManger em = emf.createEntityManger();
//JPA의 모든 데이터 변경은 트랜잭션 안에서 일어난다
em.getTransaction.begin();

//객체를 저장한 상태 (영속)
em.persist(member);
  • 엔티티 매니저를 통해 영속성 컨텍스트에 접근
  • 엔티티 매니저를 통해 member 엔티티 객체를 영속성 컨텍스트에 저장한다
  • 영속성 컨텍스트 안에서 member 엔티티 객체가 관리되고있다

영속 상태가 된다고해서 바로 DB에 쿼리가 날라가는 것이 아니다!
DB에는 이후에 커밋해야 저장된다!

준영속, 삭제

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

//객체를 삭제한 상태(삭제), db 삭제를 요청하는 것
em.remove(member);

영속성 컨텍스트의 이점

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

🔴 엔티티 조회, 1차 캐시

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

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

영속성 컨텍스트는 내부에 1차 캐시라는 것을 들고있다
1차캐시
Key) @Id : DB PK로 매핑한 필드
Value) @Entity : JPA가 관리하는 엔티티 객체

이렇게 하면 무슨 장점이 있지?

🟠 1차 캐시에서 조회

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

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

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

JPA는 엔티티 조회해올 때 1차캐시부터 뒤진다.
1차 캐시에서 해당되는 키값을 가진 엔티티가 있다면, 1차 캐시에서 조회해온다.

🟡 데이터베이스에서 조회

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

1차 캐시에 해당되는 키값을 가진 엔티티가 없다면, 그때 DB에 조회 쿼리 날려서 가져온 후 1차캐시에 엔티티 저장한다.

1차캐시에 저장된 엔티티를 반환한다.

(캐싱 전략의 read through 같은 느낌?)

사실 이게 그렇게 큰 성능상 이점은 아님
엔티티 매니저는 데이터베이스 트랜잭션 단위로 생성하고, 트랜잭션이 끝날때 같이 종료시켜버림
클라이언트 요청이 들어와서 하나의 비즈니스 로직이 끝나버리면 영속성 컨텍스트를 지움 (1차 캐시도 날라감) - 따라서 그 찰나의 순간에만 이득이 있음, 여러명의 고객이 사용하는 그런 캐시가 아니다.

영속 엔티티의 동일성 보장

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

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

1차 캐시로 반복가능한 읽기(Repeatable Read) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공

무슨 말이냐면 마치 자바 컬렉션에서 조회하듯이, JPA가 영속 엔티티의 동일성을 보장해준다는 것

가능한 이유가 바로 1차캐시가 있기 때문이다

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

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

em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 DB에 보내지 않음
//JPA가 쿼리 쭉쭉 쌓고 있는다

//커밋하는 순간 DB에 INSERT SQL을 날린다
transaction.commit(); //트랜잭션 커밋

  • em.persist(memberA);로 memberA를 1차캐시에 넣는다
  • 1차캐시에 저장됨과 동시에 JPA가 해당 엔티티를 분석해서 SQL을 생성한다.
  • 생성한 SQL을 쓰기지연 SQL 저장소라는 곳에 쌓아둔다
  • memberB도 1차캐시에 넣는다
  • INSERT SQL 생성해서 쓰기 지연 SQL 저장소에 쌓는다

그럼 쿼리 도대체 언제 날라감??

트랜잭션을 커밋하면, 커밋하는 시점에 쓰기 지연 SQL 저장소에 있던 쿼리들이 flush되면서 날라간다.
그리고 실제 DB 트랜잭션이 커밋된다.

굳이 왜 이렇게 하는 것일까? 그냥 바로 바로 날려도 되지 않나?

여기서 바로 버퍼링이라는 기능이 나온다!

만약 매번 엔티티 영속화할 때마다 DB에 쿼리를 날린다고 해보자.
그렇게되면 아예 최적화할 수 있는 여지 자체가 없어진다.
사실 데이터베이스에 아무리 데이터 많이 집어넣어도 커밋 안하면 말짱 꽝이다. (DB 반영이 안되니까) 따라서 커밋하기 직전에만 INSERT 치면 된다.
커밋하기 직전에 위의 예시에서 보면 쓰기지연 SQL저장소에 쿼리들이 쌓여있다.
이 쿼리들을 한번에 날릴 수 있다. HOW? JDBC Batch사용

그냥 Batch사용하면 굉장히 코드가 복잡하지만, Hibernate같은 경우 batch_size 옵션 한줄로 적용할 수 있다!!!

엔티티 수정 - 변경감지

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

어떻게 저런 것이 가능한걸까?

  • JPA는 커밋하는 시점에 내부적으로 flush()가 호출된다
  • 영속성 컨텍스트 flush() 호출되면, JPA는 1차캐시에 저장된 엔티티와 스냅샷을 비교한다
  • 스냅샷은 최초로 1차캐시에 들어온 상태를 저장해둔 것이다
  • 만약 스냅샷과 다른 부분이 있다면 JPA는 UPDATE 쿼리를 쓰기지연 SQL저장소에 저장한다
  • 마지막으로 해당 쿼리를 DB에 반영하고 (flush)
  • 커밋을하고 마친다

엔티티 삭제

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

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

플러시

영속성 컨텍스트의 변경내용을 데이터베이스에 반영하는 것

영속성 컨텍스트의 쿼리들을 DB에 날려주는 것이라고 보면됨
데이터베이스 트랜잭션이 커밋되면 자동으로 flush가 발생한다고 보면 된다.

플러시 발생

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

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

  • em.flush() : 직접 호출
  • 트랜잭션 커밋 : 플러시 자동 호출
  • JPQL 쿼리 실행 : 플러시 자동 호출

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

memberA, B, C 1차캐시에만 저장하고 바로 JPQL로 DB에서 모든 member 객체 조회해오면 ABC값이 조회될까?

당연히 안됨! DB에 반영안됐는데 될 수가 없음

그렇기 때문에 JPQL이 호출되면 실행전에 먼저 현재 1차캐시 상태를 DB에 반영하는 flush가 자동으로 호출되는 것이다!!

정리

플러시는!!

  • 영속성 컨텍스트를 비우지 않음
  • 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화
  • 트랜잭션이라는 작업 단위가 중요 -> 커밋 직전에만 동기화하면 된다!

준영속 상태

  • 영속 -> 준영속
  • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리 (detached)
  • 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다

준영속 상태로 만드는 방법

  • em.detach(entity) : 특정 엔티티만 준영속 상태로 전환
  • em.clear() : 영속성 컨텍스트를 완전히 초기화
  • em.close() : 영속성 컨텍스트를 종료

해당 게시글은 인프런 김영한님의 <자바 ORM 표준 JPA 프로그래밍 - 기본편>을 듣고 정리한 내용입니다.

profile
백엔드 꿈나무 🐥

0개의 댓글