자바 ORM 표준 JPA

jungnoeun·2022년 12월 16일
0

spring

목록 보기
21/24

강의 목표

  1. 객체와 테이블을 제대로 설계하고 매핑하기
  • 기본키와 외래키 매핑
  • 1:N, 1:1, N:M 매핑
  • 성능 고려
  1. JPA 내부 동작 방식 이해



JPA 기초 기능

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        // 위 코드가 뜨면 디비와 연결이 왼다.

        // em은 데이터베이스 커넥션이라고 생각해도 된다.
        EntityManager em = emf.createEntityManager(); //생성, 일관적인 단위 행동을 할떄마다 em을 만들어줘야 한다.
        EntityTransaction tx = em.getTransaction(); // em은 트랜잭션내에서 실행해야 디비에 적용된다.
        tx.begin();

        try {
            //code
            // 회원 등록
           Member member = new Member();
            member.setId(2L);
            member.setName("HelloB");
            em.persist(member); // JPA에 저장

            // 회원 수정
            Member findMember = em.find(Member.class, 1L);
            System.out.println("findMember.id = " + findMember.getId());
            System.out.println("findMember.name = " + findMember.getName());

            // 회원 삭제
           Member findMember = em.find(Member.class, 1L);
            em.remove(findMember); 

            // 회원 단건 조회 -> 따로 em.persist()를 안해줘도 된다.
            Member findMember = em.find(Member.class, 1L);
            findMember.setName("HelloJPA");

            // 회원 전체 조회
            List<Member> result = em.createQuery("select m from Member as m", Member.class)
                    .setFirstResult(5)
                    .setMaxResults(8)
                    .getResultList();
            for (Member member : result) {
                System.out.println("member.name = " + member.getName());
            }

            tx.commit(); // 커밋발생시 데이터베이스에 반영됨.
        }catch (Exception e) {
            tx.rollback();
        }finally {
            em.close(); // em이 내부적으로 데이터베이스 컬랙션을 물고 동작해서 꼭 닫아줘야 한다.
        }

        emf.close();
    }
}



  • 위의 코드중 회원 단건 조회 코드를 보면, setter함수로 값을 바꾸로 따로 em.persist(findMember); 로 따로 저장해주지 않는다. 그 이유는 JPA를 통해서 엔티티를 가지고 오면 JPA가 그 엔티티를 관리한다. 그래서 트랜잭션이 커밋하는 시점에 변경된 점이 있는지 JPA가 체크한다. 그래서 바뀐 점이 있으면 트랜잭션 커밋전에 update쿼리를 만든다. 그리고 나서 트랜잭션이 커밋되면 update쿼리가 DB에 보내진다.



주의해야 할점

  • 엔티티 매니저 팩토리는 하나만 생성해서 애플리케이션 전체에서 공유
  • 엔티티 매니저는 쓰레드간에 공유 X
    -> 고객요청이 올때마다 생성되고, 다 쓰면 버려진다.
  • JPA의 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.







🚌 영속성 컨텍스트

🧶 영속성 컨텍스트란?

  • JPA를 이해하는데 가장 중요한 용어
  • "엔티티를 영구 저장하는 환경"
  • EntityManager.persist(entity);
    -> 위 코드를 실행하면, 바로 DB에 저장되는 것이 아니라, 영속성 컨텍스트에 저장된다.
  • EntityManager 를 통해 영속성 컨텍스트에 접근




🌈 엔티티의 생명주기

🏳 비영속 (new/transient)

  • 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
    → 최초로 객체를 생성한 상태
  • 객체를 생성하고 값을 세팅(setXX())만 한 상태이다.
  • JPA와 전혀 관련없는 상태이다.

🚩 영속 (managed)

  • 영속성 컨텍스트에 관리되는 상태
    em.persist() 를 하면 영속상태가 된다.
    em.persist()는 디비에 저장되는 것이 아니다.
    그럼 언제 디비에 저장이 되는 것일까?
    => 디비에 저장이 되려면 sql이 날아가야 한다.
    영속상태가 된다고 해서 디비에 바로 쿼리가 날라가는 것이 아니다.
    => 바로 트랜잭션이 커밋되는 시점에 영속성 컨텍스트에 있는 것이 디비에 쿼리로 넘어간다.

🏁 준영속 (detached)

  • 영속성 컨텍스트에 저장되었다가 분리된 상태
    em.detached(entity) 를 하면 영속성 컨텍스트에서 지운다.
  • 즉, JPA와 아무 관계가 없어지는 것이다.

🏴 삭제 (removed)

  • 삭제된 상태
    em.remove(entity) 로, 디비에서 값을 지우는 것이다.

한줄 정리

영속성 컨텍스트는 어플리케이션과 디비 사이에 있는 어느 장소이다.




✨ 영속성 컨텍스트의 장점들

🧡 1차 캐시

  • 영속성 컨텍스트는 내부에 1차 캐시를 가지고 있다.
  • 1차 캐시는 @Id값과 Entity값이 key&value형식으로 되어 있다. 이때 @Id 값은 DB의 pk로 매핑된다.

❤ 1차 캐시의 장점

1. 빠른 속도의 조회
→ JPA는 em.find(entity); 로 조회할때, 먼저 영속성 컨텍스트의 1차캐시에 값이 있는지 찾는다. 1차 캐시에 값이 있으면 그대로 가져가고, 1차캐시에 값이 없으면 데이터베이스에서 값이 있는지 조회한다. 그리고 DB에 조회해온 값을 1차캐시에 저장한다. 그리고나서 찾은 값을 반환한다.

  • 1-1) em.persist(entity); 로 인한 쿼리실행 단축
    아래 코드를 실행해보면 DB에 select 쿼리가 나가지 않는다. em.persist(entity);를 실행해서 이미 영속성 컨텍스트의 1차 캐시에 member가 저장되었다. 그래서 em.find(member); 를 하면 1차 캐시에서 값을 찾을 수 있다. 그래서 따로 DB에서 값을 찾지 않아도 된다.
	Member member = new Member();
	member.setId("member1");
	member.setUsername("회원1");
    
	//1차 캐시에 저장됨
	em.persist(member);
    
	//1차 캐시에서 조회
	Member findMember = em.find(Member.class, "member1");
 
	tx.commit();

// 사진 12

  • 1-2) em.find(member);를 두번 실행 -> select 쿼리 한번 실행
    아래 코드를 실행하면 DB로의 select 쿼리는 한번만 실행된다. JPA가 첫번째 em.find(Member.class, 20L);에서 DB에서 Member값을 가지고 오면서 영속성 컨텍스트에 올린다. 그리고 나서 두번째로 똑같은 Member값을 조회하면 이미 영속성 컨텍스트에 찾는 값이 있어서 따로 DB에 select 쿼리를 보내서 찾지 않고, 1차 캐시에서 바로 값을 가져온다.
Member findMember1 = em.find(Member.class, 20L);
Member findMember2 = em.find(Member.class, 20L);
findMember1 == findMember2




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

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); //동일성 비교 true
  • 위 코드를 실행하면 두개의 값은 서로 같다.
    -> 자바 컬랙션에서 값을 가져오면 주소가 같은 것처럼 JPA가 영속 엔티티의 동일성을 보장해준다.
    (엔티티의 == 비교 결과 보장)
    -> 즉, 같은 트랜잭션내에서의 ==비교는 보장된다.




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

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
  • 위 코드를 보면, transaction.commit();이 실행되기 전까지 em.persist(entity);들을 모았다가 트랜잭션이 커밋되면 DB에 insert 쿼리가 나간다. 즉, transaction.commit();이 실행되기 전에 em.persist(entity); 를 실행하면 영속성 컨텍스트의 1차캐시에 값이 저장된다. 그리고 영속성 컨텍스트의 쓰기 지연 SQL저장소에 insert 쿼리가 생성되어 저장된다.

  • transaction.commit(); 를 실행하면 쓰기 지연 SQL저장소에 있던 쿼리들이 em.flush()가 되면서 DB에 넘어간다. 그리고나서 실제 DB 트랜잭션이 커밋된다.

  • 이렇게 쿼리들을 모았다가 한번에 DB에 보내는 기능을 "버퍼링"이라고 한다. 이를 잘 사용하면 성능을 좋게 할 수 있다.
    -> 사용예시 ) <property name="hibernate.jdbc.batch_size" value="10"/>
    10개씩 데이터베이스에 보내고 커밋을 하는 것




💙 4. 엔티티 수정 - 변경 감지 (=더티 체킹)

  • 앞에서 봤던 단건조회 후 값을 바꾸고 나서 em.persist(entity); 를 해주지 않아도 값이 바뀌었던 것이 바로 JPA가 변경감지를 통해 값을 수정해주었기 때문이다.
  • JPA의 목적은 자바의 컬랙션을 다루듯 객체를 다루는 것이다.
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) 이런 코드가 있어야 하지 않을까? -> NONO
transaction.commit(); // [트랜잭션] 커밋
  • 변경 감지가 동작하는 방법
    JPA는 데이터베이스 트랜잭션을 커밋하는 시점에 내부적으로 flush()가 실행된다. 그러면 엔티티와 스냅샷을 비교한다. 1차 캐시에는 @Id값, Entity값, 스냅샷값 이 있다. 스냅샷 값은 값을 읽어온 최초 시점의 상태로 변경되기 이전의 값을 의미한다. 즉, 최초로 영속성 컨텍스트의 1차 캐시에 들어온 상태이다. 그래서 엔티티와 스냅샷의 값이 달라서, 엔티티의 값이 바뀐것을 JPA가 알게되면, update 쿼리를 만들어서 쓰기 지연 SQL저장소에 넣어둔다. 그리고 이 쿼리를 DB에 반영하고 커밋한다. 이 과정을 변경감지라고 한다. 즉, JPA는 값을 바꾸면 트랜잭션이 커밋되는 시점에 변경을 반영한다.




💜 5. 엔티티 삭제

변경감지와 동작방식이 같다. 단지 트랜잭션 커밋시점에 delete쿼리가 나간다.




🤍 플러시

  • 영속성 컨텍스트의 변경내용을 데이터베이스에 반영
    → 쌓아뒀던 쿼리들을 데이터베이스에 적용하는 것이다.
  • 즉, 영속성 컨텍스트의 변경사항과 데이터베이스를 맞추는 과정이다.
  • 변경 감지의 동작과정에서 봤던 flush()를 의미한다.

플러시가 발생하면 어떤 일이 발생할까?

  • 변경 감지
  • 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록
  • 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제 쿼리)
  • 플러시가 발생한다고 해서 DB 트랜잭션이 커밋되는 것은 아니다.
    -> transaction.commit();이 실행되어야 DB 트랜잭션이 커밋된다. 즉, DB에 변경이 반영되려면 transaction.commit();이 발생해야 한다.

💗 flush, flush(), commit() 차이
1. 엔티티와 스냅샷 비교 후 변경된 것에 대한 SQL 생성
2. 생성된 SQL을 쓰기 지연 SQL 저장소에 등록
3. 쓰기 지연 SQL 저장소에 등록된 쿼리를 DB로 전송

  • flush() : 1+2+3
  • flush : 3
  • 그리고 나서 commit까지 진행해야 DB에 반영된다.

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

  1. em.flush() - 직접 호출
    -> flush() 를 실행한다고 해서 1차 캐시가 다 지워지지는 않는다. 1차 캐시는 그대로 유지가 되고, 영속성 컨텍스트의 쓰기 지연 SQL저장소에 있는 쿼리들이 바뀐다. 그리고 이 바뀐 쿼리들이 DB에 적용이 된다.
    관련코드
Member member = new Member(200L, "member200");
em.persist(member);
// 미리 데이터베이스에 적용하거나 미리 쿼리가 적용하고 싶으면 em.flush();
em.flush(); // tx.commit();전에 insert쿼리가 실행된다.
System.out.println("================");
tx.commit(); // 커밋발생시 데이터베이스에 반영됨.
  1. 트랜잭션 커밋 - 플러시 자동 호출
    -> 트랜잭션이 커밋되면 플러시는 자동 호출된다.

  2. JPQL 쿼리 실행 - 플러시 자동 호출

  • JPQL 쿼리가 실행되면 플러시가 자동으로 호출되는 이유는 무엇일까?
    -> JPQL은 sql이 그대로 변역이 되어서 실행된다. 그래서 잘못하면 문제가 생길 수 있다. JPA는 이를 방지하고자 기본적으로 JPQL쿼리를 실행할때 플러시(flush)를 무조건 날린다. 그래서 아래 코드에서 JPQL을 실행시 A,B,C를 다 조회할 수 있다.
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();



플러시를 하면 생기는 일

플러시는!

  • 영속성 컨텍스트를 비우지 않음
  • 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화
  • 트랜잭션이라는 작업 단위가 중요 → 커밋 직전에만 동기화 하면 됨
    → 플러시라는 매커니즘이 동작가능한 이유는 트랜잭션이란 작업단위가 있기 때문이다.






⛄ 준영속 상태

  • 영속상태가 되는 두가지 경우
  1. em.persist(entity); 를 하면 영속상태가 된다.
  2. em.find(entity); 해서 가져오는 과정에서, 영속성 컨택스트에 없을때 디비에서 가져와서 1차캐시에 넣을때도 영속상태가 된다.

준영속 상태란?

  • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)되는 것
  • 영속성 컨텍스트가 제공하는 기능을 사용 못함

💧 준영속 상태로 만드는 방법

em.detach(entity)

  • 특정 엔티티만 준영속 상태로 전환
  • 더이상 JPA(영속성 컨택스트)가 entity를 관리해주지 않는다. 트랜잭션 커밋을 해주어도 아무일도 일어나지 않는다.

em.clear()

  • 영속성 컨텍스트를 완전히 초기화
  • 1차 캐시에 있는 내용들도 다 지워진다.

em.close()

  • 영속성 컨텍스트를 종료
profile
개발자

0개의 댓글