[JPA] 자바 ORM 표준 JPA 프로그래밍 ch.3

박지운·2023년 5월 4일
2

김영한님의 '자바 ORM 표준 JPA 프로그래밍'을 읽고 정리한 글입니다.


3장 JPA 시작

JPA에서 제공하는 기능은 엔티티와 테이블을 매핑하는 설계 부분과 매핑한 엔티티 실제 사용하는 부분으로 나눌 수 있다.
엔티티 매니저는 엔티티와 관련된 모든 일을 하는 관리자. 개발자 입장에서는 가상의 데이터베이스로 구현

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

데이터베이스를 하나만 사용하는 애플리케이션은 EntityMangerFactory를 하나만 생성한다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook") 

persistence.createEntityManagerFactory("jpabook") 호출시 META-INF/persistence.xml에 있는 정보를 바탕으로 EntityManagerFactory를 생성한다.

팩토리는 비용이 매우 크지만 매니저를 생성하는 비용은 적다.
그리고 엔티티 매니저는 동시성 문제로 여러 스레드가 동시에 접근하면 안된다.
(race condition)
-> DB에 데이터를 CRUD할 때 여러 스레드가 데이터를 서로 바꿈


3.2 영속성 컨텍스트란?

영속성 컨텍스트란 '엔티티를 영구 저장하는 환경

em.persist(member);

이 메소드는 단순히 회원 저장하는 것이 아닌 회원엔티티를 영속성 컨텍스트에 저장하는 것이다.


3.3 엔티티의 생명주기

✔ 4가지 상태

엔티티에는 4가지 상태가 존재한다.

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

✔ 비영속

앤티티 객체를 막 생성했을 때 순수한 객체 상태이며 저장하지 않았다. 이때를 비영속 상태라 한다.

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

✔ 영속

엔티티 매니저를 통해 영속성 컨텍스트에 저장했다. 영속성 컨텍스트가 관리하는 엔티티는 영속 상태라 한다.

em.persist(member);

✔ 준영속

영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 된다.
준영속 상태가 되는 3가지 메소드

  • em.detach() : 직접 사용하는 일이 거의 없다.
  • em.close()
  • em.clear()

참고 : https://girawhale.tistory.com/122

✔ 삭제

엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제

em.remove(member);

3.4 영속성 컨텍스트의 특징

  • 영속성 컨텍스트와 식별자 값
    - 영속 상태는 식별자 값(@Id로 테이블의 기본키와 매핑한 값)이 있어야 한다.
  • 영속성 컨텍스트와 데이터베이스 저장
    - JPA는 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티 데이터베이스에 반영 -> 플러시
  • 영속성 컨텍스트의 엔티티 관리 장점
    -1차 캐시
    -동일성 보장
    -트랜잭션 지원 쓰기 지연
    -변경 감지
    -지연 로딩

✔ 엔티티 조회

영속성 컨텍스트는 1차 캐시라는 내부 캐시를 가진다. 영속 상태의 엔티티는 모두 이곳에 Map(식별자 : 엔티티 객체)로 저장된다.

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

member객체를 만든 후 이 코드를 실행하면 1차 캐시에 회원 엔티티를 저장하지만 아직 데이터베이스에 저장되지 않는다.

// 엔티티 조회
Member member = em.find(Member.class, "member1");

em.find()는 1차 캐시에서 엔티티를 찾고 없으면 데이터베이스에서 조회한다.

1차 캐시, 데이터베이스에서 조회

만약 조회 엔티티가 캐시에 있다면 메모리에서 불러온다. 없다면 데이터베이스 조회 후 1차 캐시에 저장 후 엔티티 반환한다.

1차 캐시에 있다면 메모리에서 빠르게 불러올 수 있다.

영속 엔티티의 동일성 보장

영속성 컨텍스트는 성능상 이점과 동일성을 보장한다.
(em.find로 같은 엔티티를 두번 불러와도 member는 다르지 않다.)


✔ 엔티티 등록

transaction.begin(); // 트랜잭션 시작

em.persist(memberA);
em.persist(memberB);
//여기까지 쿼리문 안보냄

transaction.commit(); //트랜잭션 커밋 
//커밋 순간에 insert 쿼리문을 보낸다.

위처럼 커밋할 때 쌓아둔 쿼리를 날리는 것을 쓰기 지연이라 한다.

회원A는 영속화 되어 1차 캐시에 저장된다. 동시에 쓰기 지연 SQL저장소에 쿼리를 보관한다.
이후 트랜잭션 커밋시 매니저는 '영속성 컨텍스트를 플러시'한다.
(플러시 : 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화)

트랜잭션을 지원하는 쓰기 지연이 가능한 이유

트랜잭션을 커밋해야만 데이터베이스에 SQL이 제대로 전달할 수 있다.(?)


✔ 엔티티 수정

SQL수정 쿼리의 문제점

비즈니스 로직 확인위해 SQL을 계속 확인, SQL자체에 의존하게 된다.

변경 감지

///데이터 수정
memberA.setUsername("hi");
memberA.setAge(10);

transaction.commit():

수정 시에는 등록 persist처럼 update같은 메소드를 사용하지 않는다. 엔티티의 변경사항을 자동으로 반영하는 변경 감지기능이 있다.

JPA는 엔티티를 영속성 컨텍스트에 저장할 때 최초 상태인 스냅샷을 저장한다.이후 플러시 시점에 스냅샷과 엔티티를 비교한다.

  1. 트랜잭션 커밋 시 먼저 엔티티 매니저 내부에서 플러시 호출
  2. 엔티티와 스냅샷 비교, 변경된 엔티티 찾는다.
  3. 변경된 엔티티가 존재하면 수정 쿼리를 생성 ,지연 SQL 저장소에 보낸다.
  4. 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
  5. 트랜잭션을 커밋한다.

변경감지는 영속상태 엔티티에만 적용된다.

회원의 이름과 나이만 수정해도 모든 필드를 업데이트하는 쿼리가 전송된다.

  • 데이터 전송량이 증가하는 단점 존재
  • 수정 쿼리가 항상 같으므로 재사용 가능
  • 데이터베이스는 이저넹 한번 파싱된 쿼리 재사용 가능(?)

하이버네이트의 DynamicUpdate 확장 기능을 사용하면 일부만 수정이 가능하다.
(30개 이상의 테이블일 때 더 빠르다 but 테이블이 30개 이상? -> 거의 잘못만든 것일듯..?)

✔ 엔티티 삭제

삭제하기 위해서는 먼저 대상 엔티티를 조회해야 한다.

Member memberA = em.find(Meber.class, "memberA");
em.remove(memberA);

삭제 또한 커밋시 플러시를 호출하면 삭제 쿼리를 전달하는 과정을 거친다. em.remove시 memberA는 영속성 컨텍스트에서 제거된다.


3.5 플러시

플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.

플러시 실행시
1. 변경 감지 동작, 모든 엔티티 스냅샷과 비교해 수정 쿼리 만들어 쓰기 지연 저장소에 저장
2. 저장소의 쿼리 데이터베이스에 전송

영속성 컨텍스트 플러시 방법
1. em.flush()
2. 트랜잭션 커밋 시 플러시 자동 호출
3. JPQL 쿼리 실행 시 플러시 자동 호출

  • 직접 호출
    영속성 컨텍스트를 강제로 플러시한다. 거의 사용은 X
  • 트랜잭션 커밋 시 플러시 자동 호출
    데이터베이스 변경 내용을 SQL로 전달하지 않고 트랜잭션만 커밋하면 어떤 데이터도 반영x 꼭 플러시를 먼저 호출해야한다.
    JPA는 커밋시 플러시 자동 호출한다.
  • JPQL 쿼리 실행 시 플러시 자동 호출
    영속성 컨텍스트에 있지만 데이터베이스에 반영되지 않은 상황이 있을 수 있다.따라서 쿼리 실행시 플러시가 자동 호출된다.
    ( find()시에는 호출되지 않는다.)

✔ 플러시 모드 옵션

플러시 모드 두가지

  • FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러시(기본값)
  • FlushModeType.COMMIT : 커밋할 때만 플러시(성능 최적화 시 사용)

3.6 준영속

준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
영속-> 준영속 만드는 방법 3가지

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

✔ 엔티티를 준영속 상태로 전환: detach()

//엔티티 생성, 초기화 가정, 비영속 상태
Member member = new Member();

//회원 엔티티 영속 상태
em.persist(member);

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

메소드 호출하는 순간부터 1차 캐시에서 제거 후 관련 SQL도 저장소에서 제거한다.

✔ 영속성 컨텍스트 초기화: clear()

em.clear()는 영속성 컨텍스트를 초기화해서 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만든다.

///엔티티 조회, 영속 상태
Member memberB = em.find(Member.class, "memberA");

em.clear();

//준영속 상태
member.setUsername("김벡수");

준영속 상태이므로 변경 감지는 동작하지 않는다.

✔ 영속성 컨텍스트 종료: close()

해당 영속성 컨텍스트가 관리하는 모든 엔티티가 준영속 상태가 된다.
아예 쓰기저장소와 1차캐시가 사라진다.
(영속 상태 엔티티는 주로 영속성 컨텍스트가 종료되면서 준영속 상태가 된다.)

✔ 준영속 상태의 특징

준영속 상태인 회원 엔티티

  • 거의 비영속 상태에 가깝다.
    1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩 동작 X
  • 식별자 값을 가지고 있다.
    비영속은 식별자 값이 없지만 준영속 상태는 이미 영속 상태였으므로 식별자 값이 있다.
  • 지연 로딩 불가능
    LAZY LOADING은 실제 객체 대신 프록시 객체를 로딩하고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러온 방법이다. 때문에 지연 로딩을 사용 불가능하다.

✔ 병합: merge()

준영속 -> 영속 : 병합 사용


public class ExamMergeMain {
    static EntityManagerFactory emf =
            Persistence.createEntityManagerFactory("jpabook");

    public static void main(String args[]) {
		//준영속 상태 엔티티 반환 받음
        Member member = createMember("memberA", "회원1");

        //준영속 상태에서 변경 --> 변경이 불가능
        member.setusername("헤롱");

		//merge
        mergeMember(member);
    }

    static Member createMember(String id, String username) {
        //영속성 컨텍스트1 시작
        EntityManger em1 = emf.createEntityManager();
        EntityTransaction txl = em1.getTransaction();
        tx1.begin();

        Member member = new Member();
        member.setId(id);
        member.setUsername(username);

        em1.persist(member);
        tx1.commit();

        em1.close();
        //영속성 컨텍스트1 종료
        //member 엔티티는 준영속 상태가 된다.
        return member;
    }

    static Member mergeMember(Member member) {
        //영속성 컨텍스트2 시작
        EntityManagerFactory em2 = emf.createEntityManager();
        EntityTransaction tx2 = em2.getTransaction();

        tx2.begin();
        //영속 상태 엔티티가 반환
        Member mergemember = em2.merge(member);
        // 커밋 시 수정 회원명이 데이터베이스에 반영된다.
        tx2.commit();

        //준영속 상태
        System.out.println("member = " + member.getUsername());

        //영속상태
        System.out.println("mergeMember = " + mergeMember.getUsername());
        System.out.println("em2 contains member = " + em2.contains(member));
        System.out.println("em2 contains mergeMember  =  " + em2.contains(mergeMember()));
        
        em2.close();
        //영속성 컨텍스트2 종료
    }
}
	
  출력 결과
  member = 헤롱??
  mergeMember = 헤롱
  em2 contains member = false
  em2 contains mergeMember = true

merge() 동작 방식

1.merge() 실행
2.파라미터의 준영속 엔티티 식별자 값이 1차 캐시에서 엔티티 조회
(만약 없으면 데이터베이스에서 엔티티 조회 후 1차 캐시에 저장)
3.조회한 영속 엔티티(mergeMember)에 member의 값을 넣는다. 이때 "회원1"이 "헤롱"으로 바뀐다.
4. mergeMember 반환

이후 commit시 변경감지 기능이 동작해 데이터베이스에 반영된다.

병합은 비영속 엔티티도 영속 상태로 만들 수 있다.


3.7 정리

  • 엔티티 매니저는 팩토리에서 생성한다. 엔티티 매니저를 만들면 내부에 영속성 컨텍스트도 같이 만들어진다. 이는 매니저를 통해 접근 가능하다.
  • 영속성 컨텍스트는 애플리케이션과 데이터베이스 사이의 객체 보관하는 가상의 데이터베이스다.
    - 1차 캐시, 동일성 보장, 트랜잭션지원 쓰기지원, 변경 감지, 지연 로딩
  • 영속성 컨텍스트 엔티티는 플러시 시점에 데이터베이스 반영, 일반적으로 트랜잭션 커밋 시 플러시
  • 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라 한다. 관리하지 못하는 엔티티는 준영속 상태가 된다.
profile
앞길막막 전과생

2개의 댓글

comment-user-thumbnail
2023년 5월 6일

꾸준히 하는 모습이 보기 좋아요 ~~
화이팅 ^^!

1개의 답글