JPA에서 가장 중요한 2가지
- 객체와 관계형 데이터베이스 매핑하기
- 영속성 컨텍스트
실제 JPA가 내부적으로 어떻게 동작하는지
이해하려면
영속성 컨텍스트
에 대해 공부해야 한다.
엔티티 매니저 팩토리 / 엔티티 매니저
웹 어플리케이션에서 엔티티 매니저 팩토리
가
고객의 요청이 올 때마다 엔티티 매니저
를 생성한다.
엔티티 매니저는 내부적으로 DB 커넥션
을 사용해서 DB를 사용
한다.
그럼 영속성 컨텍스트는 뭐야?
엔티티를 영구 저장하는 환경
EntityManager.persist(entity);
엔티티
를 DB에 저장하는 것이 아니라, 영속성 컨텍스트에 저장
한다는 뜻에 주의하자.
영속성 컨텍스트는 논리적인 개념으로, 눈에 보이지 않는다.
엔티티 매니저를 통해서 영속성 컨텍스트에 접근한다.
J2SE 환경
에서는 엔티티 매니저와 영속성 컨텍스트가 1:1
이다.
J2EE, 스프링 프레임워크 같은 컨테이너 환경
에서는 엔티티 매니저와 영속성 컨텍스트가 N : 1
이다.
참고 )
J2SE (Standard Edition)
- 가장 보편적으로 쓰이는 자바 API 집합체이다.
- 예전에는 J2SE로 불렸으나 6.0 이후
Java SE
로 변경되었다. - 일반 자바 프로그램 개발을 위한 용도로 사용되며, 스윙이나 AWT 같은 GUI 방식의 기본 기능이 포함된다.
J2EE (Enterprise Edition)
- 자바를 이용한 서버측 개발을 위한 플랫폼이다.
- 전사적 차원(대규모 동시접속과 유지가 가능한 다양한 시스템의 연동 네트워크 기반 총칭)에서 필요로 하는 도구로 EJB, JSP, Servlet, JNDI 등의 기능을 지원하고 WAS를 이용하는 프로그램 개발 시 사용된다.
엔티티 생명주기
비영속 (new / transient)
영속성 컨텍스트와 전혀 관계가 없는 새로운
상태
Member memeber = new Member();
member.setId(1L);
member.setUserName("건희");
영속 (managed)
영속성 컨텍스트에 관리
되는 상태
Member memeber = new Member();
member.setId(1L);
member.setUserName("건희");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
// 영속 상태
em.persist(member);
준영속 (detached)
영속성 컨텍스트에 저장되었다가 분리
된 상태
영속성 컨텍스트가 제공하는 기능을 사용 못 한다.
(직접 사용할 일은 거의 없지만, 내부 매커니즘을 이해하기 위해서는 중요한 개념임)
Member memeber = new Member();
member.setId(1L);
member.setUserName("건희");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
// 영속 상태
em.persist(member);
// 준영속 상태
em.detach(member);
준영속 상태로 만드는 방법
em.detach(entity)
특정 엔티티만 준영속 상태로 전환
em.clear()
영속성 컨텍스트를 완전히 초기화
em.close()
영속성 컨텍스트를 종료
삭제 (removed)
삭제
된 상태
Member memeber = new Member();
member.setId(1L);
member.setUserName("건희");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
// 영속 상태
em.persist(member);
// 삭제 상태
em.remove(member);
영속성 컨텍스트의 이점
- 1차 캐시
- 동일성 (identity) 보장
- 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
- 변경 감지 (Dirty Checking)
- 지연 로딩 (Lazy Loading)
1차 캐시
id가 1인 member를 em.persist(member);
하게 되면 member가 영속성 컨택스트의 1차 캐시
에 저장된다.
1차 캐시는 key-value 구조로 되어있으며, key
는 @Id
("1"), value
는 Entity
(member) 값이 들어간다.
이 후 em.find(Member.class, 1L);
을 실행하면 바로 DB를 조회하는 것이 아니라
영속성 컨텍스트의 1차 캐시를 먼저 조회한다.
em.find(Member.class, 2L);
를 조회한다면, 영속성 컨텍스트에 없으므로 DB에서 조회
한다.
그 후 그 정보를 1차 캐시에 저장
한 후 id가 2인 member를 반환한다.
사실 1차 캐시는 트랜잭션 단위
로 만들어지므로,
고객의 요청이 하나 들어온 후 종료되면 영속성 컨택스트도 지운다.
따라서 비즈니스 로직이 매우 복잡한 경우가 아니라면 큰 성능 이점을 얻을 수는 없다.
문제 ) 쿼리가 몇번 나갈까?
Member findMember1 = em.find(Member.class, 100L);
Member findMember2 = em.find(Member.class, 100L);
findMember1를 조회할 때 영속성 컨텍스트에 없으므로 SELECT 쿼리가 1번 나간다.
그 후 findMember2를 조회할 때는 영속성 컨텍스트에 있으므로 1차 캐시에서 값을 꺼낸다.
따라서, 쿼리는 총 1번
발생하게 된다.
영속 엔티티의 동일성(identity) 보장
Member findMember1 = em.find(Member.class, 100L);
Member findMember2 = em.find(Member.class, 100L);
findMember1 == findMember2 // ??
답은 true
가 나온다.
1차 캐시로 반복 가능한 읽기 (Repeatable READ) 등급의 트랜잭션 격리 수준을
데이터베이스가 아닌 애플리케이션 차원에서 제공한다.
(말이 어려워서 잘 이해가 안갈수도 있는데, 같은 트랜잭션 안에서는 동일성이 보장된다고 이해하자)
주의 ) 동일성 보장은 같은 트랜잭션 안에서 일어나야 함
에 주의한다.
트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 보내지 않는다.
transaction.commit(); // 트랜잭션 커밋
1차 캐시
에 member를 저장하고,
동시에 JPA가 엔티티를 분석해서 INSERT SQL을 생성
한다.
그 후 쓰기 지연 SQL 저장소
에 SQL을 저장한다.
트랜잭션이 커밋되는 시점
에 쓰기 지연 SQL 저장소
에 있던 SQL들이 flush
되면서 DB에 쿼리를 보낸다.
참고 ) JDBC batch
를 이용하면 쿼리를 일정 크기만큼 모았다가 한방에 쿼리를 보낼 수 있다. (버퍼링
)
하이버네이트
에서의 설정은 다음과 같다.
<property name="hibernate.jdbc.batch_size" value="10"/>
변경 감지 (Dirty Checking)
Member member = em.find(Member.class, 1L);
member.setName("ZZZ");
em.persist(member)
로 변경된 값을 영속성 컨택스트에 넣지 않아도, 마치 자바 컬렉션을 이용하듯이 변경이 됨.
(em.persist(member)
를 직접 쓰는 방법은 개발자의 실수를 유발할 수 있기 때문에 써서도 안된다.)
생각해보기 ) 어떻게 변경을 감지하는 걸까?
트랜잭션을 커밋하면 flush()
가 호출된다.
그 후 1차 캐시 안에 있는 엔티티
와 스냅샷
을 비교한다.
다르면 UPDATE 쿼리
를 생성하여 쓰기 지연 SQL 저장소에 저장한다.
쓰기 지연 SQL 저장소에 있던 SQL들이 flush
되면서 DB에 쿼리를 보낸 후 commit
된다.
참고 ) 스냅샷이란?
영속성 컨택스트의 1차 캐시에 들어온 최초 상태
플러시 란?
영속성 컨텍스트의 변경내용
을 데이터베이스에 반영
하는 것
플러시가 발생하면 무슨일이 생길까?
트랜잭션이 커밋
되면 플러시가 자동으로 발생되며, 다음과 같은 일이 발생한다.
- 변경 감지
- 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제 쿼리)
영속성 컨텍스트를 플러시하는 방법
em.flush() - 직접 호출
직접 쓸 일은 거의 없다. 보통 테스트 용으로 쓰인다.
Member member = new Member();
em.persist(member);
em.flush(); // 트랜잭션 커밋 전에 쿼리를 보고 싶은 경우
tx.commit();
트랜잭션 커밋 - 플러시 자동 호출
Member member = new Member();
em.persist(member);
tx.commit(); // flush 자동 호출
JPQL 쿼리 실행 - 플러시 자동 호출
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
트랜잭션이 커밋되는 시점에 DB에 SQL이 반영된다.
따라서 트랜잭션 커밋 전에 JPQL로 DB 직접 조회를 하면 DB에는 값이 없기 때문에 가져올것이 없다.
이런 문제가 발생할 수 있기 때문에 JPA는 JPQL을 실행할 때
는 무조건 flush를 호출
한다.
플러시 모드 옵션
em.setFlushMode(FlushModeType.COMMIT)
FlushModeType.AUTO
커밋이나 쿼리를 실행할 때 플러시 (
기본값
)FlushModeType.COMMIT
커밋할 때만 플러시
em.persist(memberA); em.persist(memberB); em.persist(memberC); // JPQL 실행 query = em.createQuery("select c from Car c", Car.class);
위 처럼 member가 아닌, 전혀 다른 쿼리를 날릴경우는 flush를 할 필요가 없기 때문에
플러시 해봐야 얻을 이점이 없다.
하지만 사실상 큰 도움은 안됨.
그냥 AUTO 쓰자
플러시 정리
- 영속성 컨텍스트를 비우지 않는다.
- 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화
- 트랜잭션 커밋 직전에만 동기화 하면 된다.
JPA는 엔티티에 기본 생성자가 있어야 한다.
JPA는 내부적으로 리플랙션
을 사용해서 동적으로 객체를 생성한다.
그렇기 때문에 기본 생성자
가 있어야 한다.
참고
- https://medium.com/@dnjswbf/j2se-j2ee%EC%9D%98-%EC%B0%A8%EC%9D%B4-9e474cc3e46c
- 자바 ORM 표준 JPA 프로그래밍 - 기본편
'JPA' 카테고리의 다른 글
JPA 를 공부하면서 알게 된 내용 정리 1 (0) | 2022.01.26 |
---|---|
다양한 연관관계 매핑 (0) | 2020.04.09 |
연관관계 매핑 기초 (0) | 2020.04.06 |
엔티티 매핑 (0) | 2020.04.06 |
JPA를 왜 써야 하는가? (0) | 2020.04.03 |
댓글