본문 바로가기

Web Sever 개발과 CS 기초/스프링

JPA 기본 동작과 영속성 원리 이해하기

관련 내용

<JPA 영속성 컨텍스트부터 다양한 매핑까지 완전히 이해하기>

  • (현재 글)JPA 기본 동작과 영속성 원리 이해하기

JPA 기본 동작과 영속성 원리 이해하기

  • JPA 엔티티 매핑 - 테이블, 컬럼, 기본키

JPA 엔티티 매핑 - 테이블, 컬럼, 기본키

  • JPA 연관 관계 매핑과 고급 매핑

JPA 연관 관계 매핑과 고급 매핑

  • JPA지연 로딩, 즉시 로딩 이해와 사용법 영속성전이, 고아 객체 알아보기

JPA지연 로딩, 즉시 로딩 이해와 사용법 영속성전이, 고아 객체 알아보기

  • JPA 값 타입 이해하기

JPA 값 타입 이해하기

  • JPQL, fetch join 중심으로 이해하기

JPA JPQL, fetch join 중심으로 이해하기

개요 목적

JPA는 Spring에서 RDB를 사용할 때 가장 많이 쓰는 ORM 기술이다.

이번 시간에는 JPA 기본적인 사용(CRUD) 방법와 JPA 내부적 동작 원리 영속성 컨텍스트 이해에 대한 내용을 알아본다.

EntityMangerFactory, EntityManger, EntityTransaction에 대한 이해

EntityMangerFactory → EntityManger → EntityTransaction으로 생성하는 과정을 알아보고,

각 객체가 어떤 역할과 특징을 가지고 있는 지 코드를 통해서 알아보자.

public class jpaMain {

    public static void main(String[] args) {
        //Persistence 에서 메타데이터(Driver,URL,USERNAME,PASSWORD, Dialect등) 정보를 받아서
        //EntityManagerFactory를 생성 - 애플리케이션 전체에서 공유
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        
        //엔티티매니저팩토리에서, 각 쓰레드마다 DB 동작을 할 EntityManager를 생성한다.
        //쓰레드 간 공유하지 않고, 사용한 후 버려야한다.
        //각 쓰레드마다 커넥션 트랜잭션을 유일하게 유지하기 때문이다.
        EntityManager em = emf.createEntityManager();
        
        //JPA의 모든 데이터 변경은 트랜잭션 안에서 실행한다.
        //엔테티 매니저에서 EntityTransaction 생성
        EntityTransaction tx = em.getTransaction();

        try {
            //EntityTrasaction 시작을 알리고 
            //JPA 사용해서 DB 처리를 하게 된다.
            tx.begin();
            Member member = em.find(Member.class, 1l);
            System.out.println(member.toString());
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

}

기본적인 JPA CRUD 동작 방법

사전 준비 (Entity Class 와 DB 테이블 생성)

//Entity class 작성
@Entity
public class Member {

    @Id
    private Long id;
    private String name;
}
//실제 DB 테이블 등록
create table Member ( 
 id bigint not null, 
 name varchar(255), 
 primary key (id) 
);

데이터 추가

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

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

//데이터 추가
try {
    Member member = new Member(1L, "helloA");
    //객체를 persist하는 것만으로 insert 쿼리가 나가게 된다.
    em.persist(member);
    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
emf.close();

데이터 찾기

//데이터 찾기
try {
    //em.find로 키값에 맞는 데이터를 객체로 반환해준다.
    Member findMember = em.find(Member.class, 1L);
    System.out.println(findMember.getId());
    System.out.println(findMember.getName());
    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
emf.close();

데이터 수정

//데이터 수정
try {
    Member findMember = em.find(Member.class, 1L);
    findMember.setName("HelloJpa");
    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
emf.close();

JPA를 통해서 정보를 가져오면, JPA가 관리하는 객체가 된다. 트랜잭션 커밋 시점에서 객체가 변경된 것을 확인하고 update쿼리를 날린다.

어떻게 이런 동작을 할 수 있는 지는 아래 영속성에 대한 이해에서 알 수 있다.

데이터 삭제

//데이터 삭제
try {
    Member findMember = em.find(Member.class, 1L);
    //데이터를 찾은 후 remove를 하면 delete쿼리가 나간다.
    em.remove(findMember);
    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
emf.close();

JPA의 동작 핵심 영속성 컨텍스트 이해하기

이 아래 글부터는 JPA 동작의 핵심을 담고 있는 영속성 컨텍스트 원리를 알아본다.

영속성 원리를 알아야 JPA를 예외 없이, 실무에서 편하게 사용할 수 있기 때문에 잘 파악해두어야 한다.

  • 엔티티 매니저와 1:1 매칭된 영속성 컨텍스 개념 알아보기
  • 영속성 컨텍스트 내부 동작 원리와 얻는 이점 알아보기
  • 플러시란 무엇인가
  • 준영속 상태란 무엇인가.

영속성 컨텍스트

JPA 기술에서 중요 한 두 가지 개념은 첫 번째, 객체와 관계형 데이터간의 맵핑, 그리고 영속성 컨텍스트(JPA가 내부적으로 어떻게 동작하는 지)에 이해가 있다.

위에 update 쿼리를 entity에서 가져온 값에 set으로 변경만 해주어서 실행하는 것을 보았다. 왜 단순한 코드로 SQL 작업을 할 수 있는 지 영속성 컨텍스트에 대한 이해를 통해 알아보자.

영속성 컨텍스트란

엔티티 컨텍스트를 풀어보면 엔티티 영구 저장 환경이라는 뜻이다.

쓰레드마다 엔티티 매니저가 생성되면, 엔티티 매니저와 1:1 매칭되는 영속성 컨텍스트가 생성된다.

엔티티 매니저를 통해, 객체를 영속성 컨텍스트에 올려두면 그 안에서 작업이 일어나, 관계형 데이터 베이스와 매칭하는 역할을 한다.

위에서 본 entityManger.persist(member)는 DB에 바로 쿼리를 날리는 행위가 아니라, 해당 엔티티 매니저와 연동되어 있는 영속성 컨텍스트에 객체를 저장하는 행위이다.

엔티티 생명 주기(객체와 영속성 컨텍스트간의 관계를 나타내는)

객체와 영속성 컨텍스트간의 관계를 나타내는 생명 주기 4가지에 대해서 코드를 통해 알아보자.

public class PersistenceContext {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        //엔티티 매니저를 생성하면 그에 대응하는 영속성 컨텍스트도 생성된다.
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            //1. 비영속 (new/transient)
            //단순히 객체 생성
            //영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
            Member member = new Member(100L, "jpa");

            //2. 영속 (managed)
            //DB 저장 되는 것이 아니라 영속성컨텍스트에 저장하는 단계
            //객체가 영속성 컨텍스트에 관리되는 상태
            em.persist(member);

            //3. 준영속 (detached)
            //영속성 컨텍스트에 저장되었다가 분리된 상태
            em.detach(member);

            //4. 삭제 (removed)
            //삭제된 상태
            em.remove(member);
            
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

영속성 컨텍스트 내부 동작 원리와 얻는 이점 알아보기

영속성 컨텍스트는 객체 지향 언어 자바와 관계형 데이터 베이스 사이에 위치하여, 둘 사이를 연결한다.

이런 영속성 컨텍스트가 있어서 얻는 이점에 대해서 알아보자.

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

1. 엔티티 1차 캐시에서 조회

엔티티 매니저 find()를 통해서 원하는 데이터를 찾을 때, DB에서 바로 찾지 않고, 영속성 컨텍스트(1차 캐시)에서 먼저 있는 지 확인한다.

  • 만약 영속성 컨텍스트안에 데이터가 있다면 DB select 쿼리 날리지 않고 바로 반환을 한다.
  • 영속성 컨텍스트 안에 없다면, DB에 select 쿼리를 날려 데이터를 찾고, 영속성 컨텍스트안에 저장 후에 반환 한다.(그러면 다음에 해당 데이터를 찾을 때는 DB가 아닌 1차 캐시에서 데이터를 바로 가져올 수 있다.)

영속성 컨텍스트는 하나의 요청 쓰레드에서만 유지되기 때문에, 캐시 효과가 그리 크진 않지만, 복잡한 비지니스 로직을 사용할 땐 빠르게 데이터를 찾을 수 있어 도움이 된다.

2. 영속 엔티티의 동일성 보장

1차 캐시에서 데이터를 가져오기 때문에, == 비교를 통해 데이터의 동일성을 확인할 수 있는 장점이 있다.

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

영속성 컨텍스트가 있기 때문에, 쓰기 처리를 모았다가 tx.commit되면 한번에 보낼 수 있다. 그렇게 되면 자원 절약을 통해 성능을 높일 수 있다. 코드와 그림을 통해 쓰기 지연 동작 과정을 알아보자.

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();

//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
//트랜잭션 시작
transaction.begin(); 

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

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

  1. em.persist(memberA)를 하면 바로 DB insert 쿼리를 날리는 것이 아니라, memberA를 1차 캐시와 쓰기 지연 저장소에 보관한다.
  2. em.persist(memberB)를 하면 memberB도 1차 캐시와 쓰기 지연 저장소에 보관한다.
  3. transaction.commit() 하는 순간, 쓰기 지연 저장소에 있는 정보를 바탕으로 DB에 한번에 Insert 쿼리를 날린다.

3. 엔티티 수정 변경 감지 기능

em.update()라는 기능 없이, 영속성 컨텍스트 안에 있는 객체가 변경되면, 변경을 감지해 알아서 update 쿼리를 날려준다.

마치 자바 컬랙션을 관리하듯이 엔티티 객체를 관리할 수 있게 한다.(자바 컬랙션 안의 값을 찾아서 변경하면, 다시 그 값을 컬랙션에 넣는 작업을 하지 않는 것처럼)

코드와 그림을 통해 변경 감지 과정을 알아보자.

//1 영속 엔티티 조회
//최초 얻어온 값을 스냅샷에 찍어 1차 캐시에 같이 저장해 놓는다.
Member memberA = em.find(Member.class, "memberA");

//2 영속 엔티티 데이터 수정 - 스냅샷과 값이 달라졌다.
memberA.setUsername("hi");
memberA.setAge(10);

//3 커밋 시점에서 스냅샷과 실제 엔티티 값이 다르다면
//쓰기 지연 저장소에 update sql을 저장한다.
//그리고 쓰기 지연 저장소에 있는 쿼리를 날려 커밋을 한다. 
transaction.commit();

//em.update(member) 그래서 이런 코드 없이 update sql 가능하다

4. 엔티티 삭제

update 방식과 마찬가지로 find로 찾아온 엔티티 값에 remove()를 하면, 쓰기 지연 저장소에 delete sql을 저장 한 후 commit 시점에 DB에 보낸다.

//삭제 대상 엔티티 조회 
Member memberA = em.find(Member.class, “memberA");
//엔티티 삭제
em.remove(memberA); 
//commit 시점에 delete sql 날림
transaction.commit();

플러시 발생

em.flush() 플러시를 하게 되면,

  • 변경 감지 기능이 실행
  • 수정된 엔티티 쓰기 지연 sql 저장소에 저장
  • 쓰기 지연 저장소의 쿼리를 데이터 베이스에 전송(등록, 수정, 삭제), find는 1차 캐시에 데이터가 없으면 바로바로 쿼리를 전송해서 데이터를 얻어온다.

플러시를 호출하는 방법

  • em.flush() 직접 호출
    • 플러시를 해도 영속성 컨텍스트, 1차 캐시는 삭제되지 않고, 유지된다.
    • em.clear()를 해야 영속성 컨텍스트가 완전히 비워진다.
  • 트랜잭션 커밋
  • JPQL 쿼리 실행

플러시 모드옵션

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

준영속 상태란

영속 상태의 엔티티가 영속성 컨텍스트에 분리한다.

영속성 컨텍스트가 제공하는 기능을 사용하지 못함

준영속 상태로 만드는 방법

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