본문 바로가기
Backend/JPA

[JPA] 영속성 컨텍스트

by 2245 2023. 12. 18.

출처

https://www.inflearn.com/course/ORM-JPA-Basic/dashboard

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 강의 - 인프런

JPA 를 공부하고 책을 보며 어려웠던 내용을 위주로 먼저 보았습니다. 옆에서 1:1 과외해주는 것 같이 생생하고 이해 잘되는 설명, 예제(코드)가 너무 좋았습니다. 어느 것 하나 애매함없이 모두

www.inflearn.com

 

 

목차

     

     

    JPA에서 가장 중요한 2가지

    1. 객체와 관계형 데이터베이스 매핑 (Object Relational Mapping)
    2. 영속성 컨텍스트 이해 ⭐

    해당 글은 2번 영속성 컨텍스트에 관해 작성하는 글입니다. 

     

     

     

    JPA 기본 동작 원리 이해: 엔티티 매니저 팩토리와 엔티티 매니저

     

    1. 웹 애플리케이션이 시작되면, EntityManagerFactory가 생성됩니다. 
    2. 이후 사용자의 요청이 들어올 때마다 EntityManagerFactory에서 각각의 EntityManager를 생성합니다.
    3. EntityManager는 커넥션 풀의 커넥션을 통해 DB에 접근하여 사용자 요청을 처리합니다.

     

    DB에 데이터를 저장

    EntityManager를 통해 DB에 사용자 데이터를 저장할 때 다음과 같이 작성합니다. 

     

    EntityManager.persist(entity);

    이때, 사용되는 개념이 영속성 컨텍스트입니다.

     

     

    영속성 컨텍스트?

    • "엔티티를 영구 저장하는 환경" 이라는 뜻입니다.
    • 영속성 컨텍스트는 논리적인 개념입니다.
    • 눈에 보이지 않습니다. 
    • EntityManager.persist(entity); 와 같이 사용합니다. 
      • 사실, 해당 코드를 작성하면 entity 가 DB에 저장이 된다고 생각할 수 있으나, 실제로 정확한 의미는 DB가 아니라 엔티티 영속성 컨텍스트에 영속화한다는 뜻입니다. 
      • 즉, 엔티티 매니저를 통해 영속성 컨텍스트에 엔티티를 저장한다고 이해하면 됩니다.
      • DB에는 영속성 컨텍스트를 통해 접근합니다.
      • 영속성 컨텍스트 → DB
    • JPA를 이해하는 데 가장 중요한 용어입니다.

     

    엔티티 매니저와 영속성 컨텍스트의 관계

    J2SE 환경

    • 엔티티 매니저를 생성하면, 일대일로 영속성 컨텍스트가 생성이 됩니다.
    • 쉽게 이야기하면, EntityManager 안에 보이지 않는 PersistenceContext 공간이 생성된다고 생각하면 됩니다. 

     

    J2EE, 스프링 프레임워크와 같은 컨테이너 환경

     

    • 여러 EntityManager가 한 개의 PersistentContext를 공유합니다.

     

     


    엔티티의 생명주기

    영속성 컨텍스트 내의 엔티티의 생명주기는 다음과 같이 구분됩니다.

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

     

    비영속(new/transient)

    • 객체를 생성만 했을 때의 상태입니다. 

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

     

     

    영속(managed)

    • 영속성 컨텍스트에 저장하여 관리되는 상태입니다.

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

     

     

    준영속(detached)

    • 영속 → 준영속
    • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached) 되는 것을 의미합니다.
    • 영속성 컨텍스트가 제공하는 기능을 사용하지 못합니다.
    • persist()한 후, detach() 하면 영속성 컨텍스트에서 분리됩니다.
    //엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
    em.detach(member);

    준영속 상태로 만드는 방법

    • em.detach(entity) : 특정 엔티티만 준영속 상태로 전환. 즉, 해당 엔티티는 데이터베이스 트랜잭션을 실행해도 아무런 일이 일어나지 않는다.
    • em.clear() : 영속성 컨텍스트 전체를 초기화
    • em.close() : 영속성 컨텍스트 종료

     

    삭제(removed)

    • 실제 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차 캐시에서 조회

    • 영속성 컨텍스트에 조회하고자 하는 엔티티가 저장되어 있다면 DB가 아닌 1차 캐시에서 조회합니다. 
    Member member = new Member();
    member.setId("member1");
    member.setUsername("회원1");
    
    //1차 캐시에 저장됨
    em.persist(member);
    
    //1차 캐시에서 조회
    Member findMember = em.find(Member.class, "member1");

     

     

    데이터베이스에서 조회

    • 만약, 조회하고자 하는 엔티티가 1차 캐시에 없다면, 데이터베이스에서 조회하여 1차 캐시에 저장한 후 반환합니다.
    Member findMember = em.find(Member.class, "member2");

    1. 찾고자 하는 member2가 1차 캐시에 없습니다.
    2. DB에서 조회합니다.
    3. 조회한 member2를 1차 캐시에 저장합니다.
    4. member2를 반환합니다.
    5. 이후에 다시 member2를 조회한다면, DB 조회없이 1차 캐시에 있는 member2가 반환됩니다.
    참고 사실 큰 이점이 되지 않는다.
    엔티티 매니저는 보통 데이터베이스 트랜잭션 단위로 생성됩니다. 따라서, 트랜잭션이 끝날 때 영속성 컨텍스트도 종료됩니다. 즉, 고객의 요청이 들어오고 끝나면 영속 컨텍스트를 지웁니다. 
    이득이 되는 순간은 고객의 요청이 들어오고 끝나기까지 굉장히 짧은 찰나의 순간입니다. 하지만, 비지니스 로직이 굉장히 복잡한 경우엔 도움이 될 수도 있습니다.
    참고로, 여러 명의 고객이 사용하거나, 애플리케이션 전체에서 공유하는 캐시는 JPA에서 2차 캐시라고 부릅니다. 

     

     

    영속 엔티티의 동일성 보장

    Member a = em.find(Member.class, "member1");
    Member b = em.find(Member.class, "member1");
    
    System.out.println(a==b);  //동일성 비교 true
    • 영속성 컨텍스트를 사용하지 않는다면, em.find를 통해 조회한 객체의 동일성을 비교하면 false가 반환됩니다. 
    • 하지만, 영속성 컨텍스트를 사용하면 동일 트랜잭션 내에서는 같은 객체를 반환하여 true가 반환됩니다. 
    • 즉, 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공합니다.

     

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

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

    em.persist()를 하는 시점에는 쓰기 지연 SQL 저장소에 SQL이 저장될 뿐, 실제 DB에 쿼리가 나가지 않습니다.
    commit()을 호출하는 순간 쓰기 지연 SQL에 저장된 SQL쿼리가 DB에 발생하여 커밋이 완료됩니다.

     

     

    엔티티 수정 - 변경 감지(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();  //[트랜잭션] 커밋

    변경 감지 (Dirty Checking)

    • 객체를 수정 후 em.update(member); 와 같은 코드를 작성하지 않아도 스스로 감지하여 수정 SQL을 생성합니다. 
    • 마치, 자바 컬렉션에서 객체를 꺼낸 후 수정해서 다시 넣지 않는 것과 동일한 로직으로 동작합니다.

    1. commit()을 호출하면, 내부적으로 flush()가 호출됩니다.
    2. 1차 캐시 안에는 스냅샷이 있는데, 스냅샷이란 최초로 영속성 컨텍스트에 들어온 상태를 의미합니다. 해당 스냅샷과 들어온 엔티티를 비교합니다.
    3. 엔티티와 스냅샷과 비교하여 변경된 부분을 UPDATE SQL로 생성합니다.
    4. UPDATE SQL을 데이터베이스에 반영합니다.
    5. 커밋이 완료됩니다.

     

     

    엔티티 삭제

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

     

     


    플러시(flush)

    영속성 컨텍스트의 변경 내용을 데이터베이스에 반영합니다.

     

     

    플러시가 발생한다면?

    1. 엔티티와 스냅샷를 비교하여 변경 감지
    2. 수정된 부분을 쓰기 지연 SQL 저장소에 등록
    3. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제 쿼리)

     

    플러시를 발생시키는 방법

    • em.flush() 호출 - 직접 호출
    • 트랜잭션 커밋 - 플러시 자동 호출
    • JPQL 쿼리 실행 - 플러시 자동 호출
    참고 플러시를 직접 호출해도 1차 캐시는 유지가 됩니다. 단지, 변경된 내용이 DB에 반영이 되는 것 뿐입니다.

     

     

    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, memberB, memberC를 데이터베이스 삽입하는 코드를 작성하였습니다.
    • 그런데, 중간에 JPQL을 통해 해당 객체를 조회하는 코드를 작성하면 어떻게 될까요?
    • JPQL은 SQL로 변경되어 실행됩니다. 
    • 따라서, memberA와 memberB와 memberC는 아직 데이터베이스에 반영이 되지 않았기 때문에 조회가 되지 않습니다.
    • 따라서, JPQL은 기본 모드가 플러시를 자동 호출하는 것입니다.
    참고 아래의 코드는 커밋 시점에 INSERT SQL만 실행됩니다. (1차 캐시에서 조회하기 때문에 SELECT SQL이 실행되지 않습니다.)

     

    em.persist(memberA);
    em.find(Member.class, 1000L);
    
    tx.commit();​

     

    플러시 모드 옵션 설정

    • FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러시합니다.(기본값)
    • FlushModeType.COMMIT : 커밋할 때만 플러시합니다.
    em.setFlushMode(FlushModeType.COMMIT)

     

     

    플러시 정리

    • 영속성 컨텍스트를 비우지 않습니다.
    • 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 것입니다.
    • 트랜잭션이라는 작업 단위가 중요합니다. → 커밋 직전에만 동기화하면 됩니다.

     

     

    'Backend > JPA' 카테고리의 다른 글

    [JPA] 다양한 연관관계 매핑  (0) 2023.12.18
    [JPA] 연관관계 매핑  (0) 2023.12.18
    [JPA] 엔티티 매핑  (0) 2023.12.18
    [JPA] Hello JPA  (0) 2023.12.02
    [JPA] JPA 소개  (0) 2023.12.01