JAVA ORM 표준 JPA 프로그래밍 - 김영한님 책 스터디 개인 3장 정리
3장 영속성 관리
3.1 엔티티 매니저 팩토리와 엔티티 매니저
3.2 영속성 컨텍스트란
3.3 엔티티의 생명주기
3.4 영속성 컨텍스트의 특징
3.5 플러시
3.6 준영속
3.7 정리
엔티티 매니저 팩토리와 엔티티 매니저
JPA가 제공하는 기능은 크게 2가지로 나눌 수 있다.
- 엔티티와 테이블을 매핑하는 설계 부분
- 매핑한 엔티티를 실제 사용하는 부분
엔티티 매니저는 엔티티 관리자이며 개발자는 가상의 데이터베이스 라고 생각하면 된다.
엔티티 매니저가 하는일은 다음과 같다
- 엔티티 저장
- 엔티티 수정
- 엔티티 조회
데이터베이스를 하나
만 사용하는 애플리케이션은 일반적으로 EntityManagerFactory
를 하나
만 생성한다.
- jpa를 사용하려면 우선 persisence.xml의 설정 정보를 사용하여
엔티티 매니저 팩토리
를 생성해야 한다- 이 때,
Persistence
클래스를 사용한다. - javax.persistence.Persistence- 엔티티 매니저 팩토리를 생성해서 JPA를 사용할 수 있게 준비하는 역할.
- 이 때,
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
- META-INF/persistence.xml 파일에서 이름이
jpabook
인 persistence-unit을 찾아서 엔티티 매니저 팩토리를 생성한다.- 사용이 끝난 뒤 반드시 종료 해야함.
enf.close()
;
- 사용이 끝난 뒤 반드시 종료 해야함.
*엔티티 매니저 팩토리는 생성 비용이 매우 크다. (JPA 동작 객체, 커넥션 풀 생성 등) *
그러므로 애플리케이션 전체에서 딱 한번만 생성하고 공유해서 사용
해야 한다.
- 엔티티 매니저가 필요할 때마다 엔티티 매니저 팩토리에서 엔티티 매니저를 생성하면 된다.
EntityManager em = emf.createEntityManager(); //emf = EntityManagerFactory emf
- EntityManagerFactory에서 엔티티 매니저를 생성하는 비용은 거의 들지 않는다.
EntityManagerFactory
는 여러 스레드가 동시에 접근해도 안전하다.- 그러나 엔티티 매니저는 여러 스레드가 동시에 접근하면
동시성 문제
가 발생하므로 스레드 간에 절대 공유하면 안 된다.
일반적인 웹 애플리케이션에선 하나의 엔티티 매니저 팩토리에서 다수의 엔티티 매니저를 생성한다.
그러나 엔티티 매니저는 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다
.
- 데이터베이스 연결이 필요한 시점에 커넥션을 획득한다.
- 보통 트랜잭션을 시작할 때 커넥션을 획득.
JPA 구현체(하이버네이트, 이클립스링크) 등이 EntityManagerFactory를 생성할 때 커넥션 풀을 생성한다.
그리고 엔티티 매니저가 데이터베이스를 사용할 때, 커넥션 풀에서 커넥션을 획득한다.
- 생성 비용이 비싼 이유에 포함된다.
Spring Data Jpa 의존성을 받은 경우, 엔티티 매니저 팩토리를 생성할 필요가 없다.
프레임워크가 알아서 생성하여 빈에 등록한다
영속성 컨텍스트란 (Persistence Context)
영속성 컨텍스트 : 엔티티를 영구 저장하는 환경.
영속성 컨텍스트는 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 논리적 개념이다.
엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
em.persist(member); // member 엔티티를 영속성 컨텍스트에 보관
- persist 메서드는 엔티티 매니저를 사용해서 엔티티를 영속성 컨텍스트에 저장한다고 생각하면 좋다.
- 여러 엔티티 매니저가 같은 영속성 컨텍스트에 접근할 수 있는 복잡한 상황도 있을 수 있다.
- 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어진다.
엔티티의 생명주기
엔티티에는 4가지 상태가 존재한다.
- 비영속(new / transtient) : 영속성 컨텍스트와 전혀 관계가 없는 상태. 영속성 컨텍스트에 저장되지 않은 상태
- 엔티티 객체를 생성하고, persist 하지 않은 상태. 순수한 객체 상태이다.
- 영속 (managed) : 영속성 컨텍스트에 저장되어 관리되는 상태.
- 엔티티 매니저를 통해서 persist한 상태. 영속성 컨텍스트가 관리하는 엔티티의 상태를
영속 상태
라고 한다 - persist 상태 또는 find해서 데이터베이스에서 조회해온 상태.
- 엔티티 매니저를 통해서 persist한 상태. 영속성 컨텍스트가 관리하는 엔티티의 상태를
- 준영속 (detached) : 영속성 컨텍스트에 저장되었다가 분리되어있는 상태
- 엔티티가 영속 상태에서 더이상 엔티티 컨텍스트가 관리하지 않는상태로 변한것.
- 다음 메서드들을 호출하면 엔티티 매니저가 관리하던 엔티티들은 준영속 상태가 된다.
- em.detach(엔티티) 메서드
- void em.cloer(); 메서드 : 영속성 컨텍스트 초기화
- void em.close(); 메서드 : 영속성 컨텍스트를 닫음
- 삭제 (removed) : 삭제된 상태.
- 엔티티를 영속성 컨텍스트와 데이터베이스에서 실제로 삭제한다.
- em.remove(member); 메서드
영속성 컨텍스트의 특징
영속성 컨텍스트와 식별자 값
영속성 컨텍스트는 엔티티를 식별자 값(PK, @Id 어노테이션이 붙은 id필드)로 구분한다.
영속상태는 식별자 값이 반드시 있어야 한다. 식별자 값이 없으면 예외가 발생한다.
- JPA는 구현체별로 던져지는 예외가 상이하다.
- 식별자를 할당하지 않고 엔티티 객체를 저장하려 하면 IdentifierGenerationException을 감싼 PersistenceException 예외가 발생
- 아니면, 다음과 같은 예외가 발생한다. org.hibernate.AnnotationException: No identifier specified for entity
- @Id 어노테이션의 패키지는 javax.persistence.Id; 이다.
영속성 컨텍스트와 데이터베이스에 저장
JPA는 보통 트랜잭션이 커밋되는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영
한다.
이것을 플러시(flush)
라고 한다.
- 엔티티매니저.flush() 메서드로 직접 플러시 할 수도 있다.
- 실제 플러시 되는 시점이나, 영속성 컨텍스트에 엔티티가 존재하지 않을 때 조회하는 경우 SQL로 변환되어 데이터베이스에 쿼리가 나간다.
영속성 컨텍스트가 엔티티를 관리하면 다음과 같은 장점이 있다
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
- 지연 로딩
1차 캐시
영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 1차 캐시라 한다.
- 캐시는 메모리의 한 종류이며 빠르게 조회할 수 있다는 특징을 가지고 있다.
- 여기서는 논리적인 개념으로, 캐시에 값이 있으면 빠르게 조회할 수 있다는 뜻이기도 하다.
em.persist(엔티티); 메서드를 호출하면 영속성 컨텍스트 내
의 1차 캐시에 엔티티를 저장한다.
- 1차 캐시는 영속성 컨텍스트의 내부이지, 데이터베이스가 아니다. 아직 데이터베이스에 저장된건 아니다.
- 데이터베이스에 반영되기 전, 애플리케이션이 올라와있는 메모리에 먼저 저장한다는 뜻이기도 하다.
1차 캐시의 key는 식별자 값이다.
(id)
em.find(엔티티, key값) 으로 엔티티를 조회할 수 있다.
1차 캐시에서 조회하게 되면, 우선적으로 1차 캐시에서 식별자 값으로 엔티티를 찾는다.
만약 찾는 엔티티가 1차캐시에 존재하면, 데이터베이스를 조회하지않고 애플리케이션 메모리에 있는 1차캐시로부터 엔티티를 조회한다.
- 일반적으로 메모리가 ssd, hdd같은 데이터베이스의 디스크 보다 속도가 빠르다.
- 데이터베이스말고 메모리에서 데이터를 읽는다는 것은 조회 속도가 훨씬 속도가 빠르다는 이기도 하다.
- 1차 캐시에(메모리)존재하지 않으면 데이터베이스(ssd, hdd) 에서 조회하고,
영속 상태
로 만들어 영속상태의 엔티티를 반환한다.
이렇게 영속성 컨텍스트 내의 1차 캐시를 이용하게 된다면, 1차 캐시 내의 영속 상태인 엔티티를 조회했을 경우 성능상 이점을 누릴 수 있다
- 실제 플러시 되는 시점이나, 영속성 컨텍스트에 엔티티가 존재하지 않을 때 조회하는 경우 SQL로 변환되어 데이터베이스에 쿼리가 나간다.
- 트랜잭션이 커밋되지 않는 순간이나 직접 플러시 하지 않은 경우에는 플러시가 호출되지 않는다고 하였다.
- 즉 트랜잭션이 커밋되지 않고 영속성 컨텍스트에 조회할 엔티티가 존재하는 경우, 트랜잭션 안에서는 몇번이고 엔티티를 조회해도 실제 데이터베이스와 통신을 안하므로 성능상 이점을 누릴 수 있게 되는것이다.
영속성 컨텍스트의 동일성 보장
앞서 2장에서 정리한 내용을 다시 보자.
JPA는 같은 트랜잭션
일 때 같은 객체가 조회되는 것을 보장한다.
그러므로 다음 코드에서는 동일성 비교가 성공한다
Member member1 = jpa.find(Member.class, 1L);
Member mebmer2 = jpa.find(Member.class, 1L);
- 위에서 설명한 오버라이딩 안한 기본 Object 클래스의 equals 메서드를 사용해도 같다고 나온다. (오버라이딩 안하면 ==비교니까)
동일성(identity) : 실제 인스턴스가 같다. 참조 값을 비교하는 == 비교의 값이 같다.
동등성(equality) : 실제 인스턴스는 다를 수 있지만 인스턴스가 가지고 있는 값이 같다. 이것은 오버라이딩을 해서 직접 구현해야한다.
영속성 컨텍스트에 존재하는 엔티티를 조회하는 경우 같은 엔티티를 반환한다.
그러므로 == 비교
가 성공한다. (같은 메모리 위의 주소에 존재하니까)
즉 영속성 컨텍스트는 성능상 이점
과 엔티티의 동일성
을 보장한다. (같은 영속성 컨텍스트 내에서. 엔티티 매니저 마다 영속성 컨텍스트가 다르다)
엔티티 등록
EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성
EntityTransaction transaction = em.getTransaction(); //트랜잭션 기능 획득
//엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다
transacton.begin(); //[트랜잭션]시작
em.persist(memberA);
em.persist(memberB) ;
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다
transaction.commit( ) ; //[트랜잭션] 커밋 -> SQL 전송
엔티티 매니저는 트랜잭션을 커밋
하기 전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소
에 INSERT SQL을 모아둔다.
그리고 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스
에 보낸다.
이것을 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
라고 한다.
- 엔티티 매니저는 em.persist(엔티티); 메서드를 하는 순간 1차 캐시에 저장하고 INSERT SQL을 생성하여 저장한다.
- 트랜잭션을 커밋하는 순간 실제 DB로 호출하는것이다.
트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스
에 보낸다는 것은 성능상 이점을 가져오는 매우 중요한 부분이다.- 트랜잭션을 커밋하지 않고 1차 캐시에서 조회한다는 것은 성능상 이점을 가져오는 것.
트랜잭션 범위 안에서 실행되므로 모두 트랜잭션을 커밋하면 함께 저장되고 롤백하면 함께 저장되지 않는다.
INSERT 쿼리를 생성할 때마다 그때그때 DB에 전달해도 커밋되지 않으면 롤백된다. 즉 실행되지 않는다.
이것이 쓰기 지연이 가능한 이유다.
엔티티 수정
em.persist()로 엔티티를 저장하면 트랜잭션이 커밋되기 전 영속성 컨텍스트 내부 쓰기 지연 저장소에 모든 쿼리가 저장된다고 하였다.
하지만 매번 엔티티를 수정할 때마다 UPDATE 쿼리를 쌓아두고 커밋할 때 DB로 전송하면 성능도 좋지 않고, 비즈니스 로직을 분석하기 위해 SQL을 계속 확인해야 한다.
이런 문제를 예방하기 위해서 JPA는 변경 감지
라는 것을 지원한다.
변경 감지(dirty cheaking)
- 변경 감지 : 엔티티의 변경사항을 데이터베이스에 자동하는 기능
JPA는 엔티티를 영속성 컨텍스트에 보관할 때 최초 상태를 복사
해서 저장해둔다.
이것을 스냅샷
이라고 한다.
변경감지의 순서
- 트랜잭션을 커밋하면 플러시(flush)가 호출된다.
- 엔티티와 스냅샷을 비교하여 변경된 엔티티를 찾는다.
- 비교 후 변경된 엔티티가 있따면 수정 쿼리를 생성하여 쓰기 지연 SQL저장소에 보관.
- 쓰기 지연 저장소의 SQL을 DB로 보낸 후 DB의 트랜잭션 커밋.
변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의
엔티티만 적용된다.
즉, 비영속, 준영속 등과 같은 상태
의 엔티티는 값을 변경해도 반영, 적용 되지 않는다.
JPA는 영속 상태의 엔티티의 일부 속성 (ex) name, age, phone 컬럼 중 name 만 )만 바뀌어도 모든 필드(name, age, phone)를 업데이트 한다.
- 모든 필드를 업데이트 하지 않는 방법은 엔티티 클래스에
@DynamicUpdate
를 사용하면 된다.
모든 컬럼을 업데이트하였을 때의 장점
- 모든 필드(컬럼)를 사용하면 수정 쿼리가 항상 같다.
- DB에서 이전에 한번 파싱된 쿼리를 재사용 할 수 있다.(DB내의 SQL 재사용)
단점
- 데이터 전송량이 증가한다. (수정되지 않은 필드도 같이 전송해서 반영하기 때문)
필드가 많거나, 저장되는 내용이 너무 크면 동적 UPDATE SQL을 생성하여 사용하면 된다.
단, 이때는 하이버네이트 확장 기능을 사용해야 한다 .
- @DynamicUpdate-
- 상황에 따라 다르지만 컬럼이 대략 30개 이상이 되면 전체 수정 쿼리보다 @DynamicUpdate가 빠르다고 한다.
- 기본 전략인 전체 업데이트를 사용하되, 최적화가 필요할 정도로 느리면 그 때 @DynamicUpdate 를 사용하면 된다.
- 한 테이블에 컬럼이 30개 이상 된다는 것은 테이블 설계상 책임이 적절히 분리되지 않았다는 가능성이 높다.
엔티티 삭제
em.remove(엔티티); 메서드 사용
엔티티 삭제시, 먼저 삭제 대상 엔티티를 조회 하고 나서 삭제 메서드를 수행해야 한다.
Member memberA = em.find(Member.class, "memberA") ; //삭제 대상엔티티 조회
em.remove(memberA) ; //엔티티 삭제
엔티티를 즉시 삭제하는 것이 아닌, 등록과 비슷하게 쓰기 지연 SQL 저장소에 등록한 후, 플러시가 되면 삭제 쿼리를 전달하고, 커밋되면 반영된다.
- remove()를 호출 하는 순간 영속성 컨텍스트에서 제거된다.(관리 대상이 아니다)
- 삭제된 엔티티는 재사용하지 말고
가비지 컬렉션
이 수거해가도록 두는것이 좋다.
플러시, flush (SQL을 DB로 전송)
플러시는 영속성 컨텍스트의 변경 내용을 실제 DB에 전송하여 반영한다.
플러시를 실행하면 다음과 같은 일이 일어난다.
- 변경 감지(dirty check)가 동작하여 영속성 컨텍스트 안에 있는 모든 엔티티를 스냅샷과 비교하여 수정된 엔티티를 찾고,
수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다. - 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송한다.
영속성 컨텍스트를 플러시
하는 방법 3가지
em.flush()
직접 호출- 테스트나 다른 프레임워크 사용할 때 제외하고 거의 사용하지 않는다.
트랜잭션 커밋
. ( 플러시 자동 호출 됨)- 쓰기 지연 SQL 저장소의 SQL을 DB로 전달하지 않고 트랜잭션만 커밋하면 어떤 데이터도 DB에 반영되지 않는다.
- 그러므로 트랜잭션 커밋 전에 꼭 플러시를 호출하여
변경 내용(쓰기 지연 SQL 저장소의 SQL)
을 전달해야 한다.
JPQL 쿼리 실행 시
, 플러시 자동 호출- JPQL 쿼리 실행 시, 쿼리 실행
중간에
알아서 플러시가 호출된다.
- JPQL 쿼리 실행 시, 쿼리 실행
플러시 모드 옵션
엔티티 매니저에 플러시 모드를 직접 지정하려면 javax.persistence.FlushModeType
을 사용하면 된다.
- FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러시 (기본값, default)
- FlushModeType.COMMIT : 커밋할 때만 플러시
em.setFlushMode(FlushdeType.COMMIT) //플러시 모드를 COMMIT 모드로 직접 설정
- 플러시는 영속성 컨텍스트에 보관된 엔티티를 지우는 것이 아니다!
- 플러시는 영속성 컨텍스트 내 의
변경 내용
들을 데이터 베이스에동기화(푸시)
하는 것이다.
준영속
준영속(detached, 분리)
: 영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것.
준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
영속 상태의 엔티티를 준영속 상태로 만드는 방법 3가지
- em.detach(entity); : 특정 엔티티를 준영속 상태로 전환
- em.clear(); : 영속성 컨텍스트 클리어. 초기화
- em.close(); : 영속성 컨텍스트 종료
영속 상태였다가 더는 영속성 컨텍스트가 관리하지 않는 상태를
준영속 상태
라 한다
영속성 컨텍스트 초기화 : clear()
em.detach(entity)
: 특정 엔티티하나를
준영속 상태로 만듬(영속성 컨텍스트에서 분리)
em.clear()
: 영속성 컨텍스트의모든 엔티티
를 준영속 상태로 만든다.- 1차 캐시에 있는 모든 영속 엔티티들을 준영속 상태로 바꿈.
영속성 컨텍스트 종료 : em.close()
엔티티 매니저의 close() 메서드를 호출하면 영속성 컨텍스트는 종료
되고,
관리되던 영속상태의 엔티티는 모두 준영속 상태
가 된다.
준영속 상태의 특징
거의
비영속 상태에 가깝다.- 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩 등 어떠한 기능도 동작하지 않는다.
식별자 값
을 가지고 있다. (PK, ID)- 비영속 상태는 식별자 값이 없을 수 있지만, 준영속 상태는
이미 한 번 영속상태 였으므로 반드시 식별자 값을 가지고 있다.
- 비영속 상태는 식별자 값이 없을 수 있지만, 준영속 상태는
- 지연로딩을 할 수 없다.
- 영속성 컨텍스트에 없으므로 지연로딩 시 문제가 발생한다.
병합 : merge()
merge()
:준영속 상태의 엔티티를 다시 영속 상태로 변경할 때 사용
// merge() 메소드의 정의
public <T> T merge(T entlty) /
// 사용할 때
Member mergeMember = em.merge(member); // member는 비영속, 준영속 상태
병합은 준영속, 비영속을 신경쓰지 않는다.
식별자 값으로 엔티티를 조회할 수 있으면 꺼내서 병합하고, 조회할 수 없으면 새로 생성해서 병합한다.
병합은 save or update
의 기능 수행을 한다.
정리
- 엔티티 매니저는 엔티티 매니저 팩토리에서 생성. 엔티티 매니저를 생성하면 내부에
영속성 컨텍스트
도 함께 생성. - 영속성 컨텍스트는 application과 DB 사이에서 객체를 보관하는 가상의 DB 역할.
영속성 컨텍스트는1차 캐시
,동일성 보장
,쓰기 지연
,변경 감지
,지연 로딩
을 지원 - 영속성 컨텍스트에 저장한 엔티티는
플러시
지점에 DB에 반영됨.- 일반적으로 트랜잭션 커밋 시
영속성 컨텍스트가 플러시
- 일반적으로 트랜잭션 커밋 시
- 영속성 컨텍스트가 엔티티를 관리하지 못하는 상태를
준영속 상태
라고 함- 준영속 상태의 엔티티는 관리를 받지 못하므로 1차 캐시, 동일성 보장, 쓰기 지연, 변경 감지, 지연 로딩 같은 기능 지원을 받지 못함.
'스터디 > JPA 프로그래밍 스터디' 카테고리의 다른 글
JPA 프로그래밍 스터디 7장 정리 (1) | 2022.10.01 |
---|---|
JPA 프로그래밍 스터디 6장 정리 (0) | 2022.10.01 |
JPA 프로그래밍 스터디 5장 정리 (0) | 2022.09.13 |
JPA 프로그래밍 스터디 4장 정리 (0) | 2022.09.13 |
JPA 프로그래밍 스터디 1장,2장 (0) | 2022.08.31 |