본문 바로가기
Back-End/Database

[JPA] 영속성 관리 및 내부 동작 방식

by 달의 조각 2022. 9. 4.
이 글은 김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 수강하며 정리한 글입니다.

 

JPA의 핵심 포인트

🌟 객체 - 관계형 DB 매핑 (ORM)
🌟 영속성 컨텍스트

 


 

JPA의 동작

  • 엔티티 매니저 팩토리: 하나만 생성해서 애플리케이션 전체 범위에서 공유한다.
  • 엔티티 매니저: 애플리케이션이 관계형 데이터베이스에서 엔터티를 관리하고 검색할 수 있도록 하는 데 사용된다. 고객의 요청마다 생성된다. 쓰레드 간 공유하지 않으므로 사용하고 버려야 한다.
public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        // code
        
        em.close();
        emf.close();
    }
}

 

JPA의 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.

try {
    Member member = new Member();
    member.setId(1L);
    member.setName("HelloA");

    em.persist(member);

    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
더보기
public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = new Member();
            member.setId(1L);
            member.setName("HelloA");

            em.persist(member);

            tx.commit();
         } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)
Member findMember = em.find(Member.class, 1L); // 조회
em.remove(findMember) // 삭제
findMember.setName("HelloJPA"); // 수정

 

🌿 JPQL

  JPA를 사용하면 엔티티 중심으로 개발을 하게 된다. 이때 검색 쿼리를 할 때 문제가 있다. 검색을 할 때에도 모든 데이터를 객체로 변환해서 검색하는 것은 불가능하므로 필요한 데이터만 불러오려면 결국 검색 조건이 포함된 SQL이 필요하다. 이때 사용하는 RDB를 대상으로 쿼리를 작성하면 해당 DB에 종속적이게 된다.

이 경우에는 JPA가 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 사용한다. SQL 문법과 유사하다. 이는 DB 테이블 대상으로 쿼리를 날리는 SQL과 달리 엔티티 객체를 대상으로 쿼리를 한다. 때문에 특정 DB에 의존하지 않는다.

 


 

영속성 컨텍스트

  영속성 컨텍스트란, 엔티티를 영구 저장하는 환경으로, 엔티티 매니저를 통해서 접근한다. 엔티티 매니저를 생성하면 내부에 영속성 컨텍스트가 1:1로 생성된다. 영속성 컨텍스트는 눈에 보이지 않는 논리적인 개념이다. 애플리케이션과 데이터베이스 사이에 위치한다고 생각하면 된다. 버퍼링, 캐시 등의 이점이 있다.

 

🌿 비영속(new / transient)

영속성 컨텍스트와 관계가 없는 새로운 상태로, 객체를 생성하기만 한 상태이다.

Member member = new Member();
member.setId(1L);
member.setName("회원1");

 

🌿 영속(managed)

영속성 컨텍스트에 의해 관리되는 상태이다. 이는 DB에 저장되었다는 의미가 아니다. DB에는 커밋을 해야 저장된다. 1차 캐시에 올라간 상태가 바로 영속 상태이므로 find()를 통해 조회한 경우에도 영속 상태가 된다.

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

em.persist(member); // DB에 쿼리가 날아가지 않는다

 

🌿 준영속(detached)

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

em.detach(member); // 특정 엔티티
em.clear() // 영속성 컨텍스트 완전히 초기화
em.close() // 영속성 컨텍스트 종료
Member member = em.find(Member.class, "1L");
member.setName("AAAAA");

em.detach(member);
tx.commit();

업데이트 쿼리가 날아가지 않는다.

 

🌿 삭제(removed)

em.remove(member); // DB에 객체 삭제를 요청

 


 

영속성 컨텍스트의 이점

 

🌳 1차 캐시

  영속성 컨텍스트 내부에 1차 캐시가 존재한다. 객체를 생성하고 persist() 하면 1차 캐시에 저장된다. @Id는 Key이고, Entity는 객체 자체이다.

find()를 하면 DB가 아닌 1차 캐시에서 엔티티를 조회한다. 만약, 1차 캐시에 객체가 존재하지 않으면 DB에서 조회하고, 찾으면 1차 캐시에 저장한 뒤 반환한다. 재조회할 경우 1차 캐시에서 찾은 후 반환한다.

보통 영속 컨텍스트는 트랜잭션 단위로 만들고, 함께 종료시킨다. 고객 요청 하나의 비즈니스 로직이 끝나면 종료되기 때문에 생명 주기가 짧다. 때문에 성능상 크게 도움이 되지는 않는다.

 

🌳 영속 엔티티의 동일성 보장

1차 캐시가 있기에 가능하다.

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

System.out.println(a == b); // true

 

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

em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 DB에 보내지 않는다

tx.commit();
// 커밋 하는 순간 DB에 INSERT SQL을 보낸다.

  영속 컨텍스트 안에는 1차 캐시 외에 쓰기 지연 SQL 저장소가 존재한다. persist()로 객체를 1차 캐시에 넣으면 동시에 JPA가 엔티티를 분석하고 INSERT 쿼리를 저장소에 쌓아 둔다. commit()을 하는 순간에 flush가 되면서 쓰기 지연 SQL 저장소에 쌓여 있던 쿼리가 DB에 실행된다.

persist()를 할 때마다 쿼리가 나가면 최적화가 어렵다. 모아 두었다가 DB에 한 번에 저장되도록 하는 버퍼링 기능을 사용할 수 있다. JDBC Batch 옵션을 통해 사이즈를 설정할 수도 있다. 성능상 이점이 있다.

 

🌳 엔티티 수정: 변경 감지

  따로 업데이트 명령을 하지 않아도 된다. 단순히 값만 변경하고 커밋을 하면 엔티티와 스냅샷을 비교해서 업데이트 쿼리를 날려 준다. 스냅샷은 데이터을 호출한 시점(1차 캐시에 들어온 시점)의 상태를 찍어 두는 것이다.

commit()을 하면 내부적으로 flush()가 실행되면서 엔티티와 스냅샷을 비교하는 과정이 발생한다. (더티 체킹, dirty checking) 변경이 된 부분을 발견하면 UPDATE 쿼리를 쓰기 지연 SQL 저장소에 넣는다.

엔티티 삭제 또한 같은 프로세스이다. DELETE 쿼리가 만들어진다.

Member member = em.find(Member.class, 1L);
member.setName("ZZZZZ");

 


 

플러시

 

  영속성 컨텍스트의 변경 내용을 데이터베이스에 반영(동기화)한다. 보통 트랜잭션이 커밋 될 때 발생한다. 변경이 감지되면 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록한다. 등록된 쿼리를 데이터베이스에 전송한다. (등록, 수정, 삭제 쿼리) 쓰기 지연 SQL 저장소에 쌓인 쿼리들이 반영될 뿐, 영속성 컨텍스트(1차 캐시)를 비우지는 않는다.

 

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

  • em.flush(): 직접 호출하는 방법이다. 커밋 이전에 즉시 DB에 반영하고 싶을 때 사용한다.
  • 트랜잭션 커밋: 자동 호출
  • JPQL 쿼리 실행: 자동 호출

 


📚 Reference

댓글