도서 '자바 ORM 표준 JPA 프로그래밍' 을 보고 책 내용과 그 이외의 부족한 부분을 채워가며 공부한 내용입니다.
키워드 : 엔티티 매니저, 엔티티 매니저 팩토리, 영속성, 준영속, 비영속
1. 엔티티 매니저 팩토리
JPA가 제공하는 기능
- 엔티티와 테이블을 매핑하는 설계 부분
- 매핑한 엔티티를 실제 사용하는 부분
- 엔티티 매니저를 통해 사용
엔티티 매니저
엔티티를 저장하고 수정하고 삭제하고 조회하는 등 엔티티와 관련된 모든 일을 처리
- 여러 스레드가 동시에 접근하면 동시성 문제 발생 -> 절대 스레드간에 공유하면 안됨
- 엔티티 매니저는 트랜잭션이 시작하기 전까지는 데이터베이스 커넥션을 사용하지 않음
엔티티 매니저 팩토리
엔티티 매니저를 만드는 공장
공장을 만드는 비용이 상당히 크기 때문에 한 개만 만들어서 애플리케이션 전체에서 공유하도록 설계
공장에서 엔티티 매니저를 만드는 것은 비용이 크지 않음
- 엔티티 매니저 팩토리에는 여러 스레드가 동시에 접근해도 안전
- EntityManagerFactory가 생성될 때 커넥션 풀을 만듬
//엔티티 매니저 팩토리 생성, 비용이 큼
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
//엔티티 매니저 생성, 비용이 거의 안들어감
EntityManager em = emf.createEntityManager();
2. 영속성 컨텍스트
엔티티를 영구히 저장하는 환경
- 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관, 관리
em.persist(member);
- persist()
- 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장
3. 엔티티 생명주기
엔티티의 4가지 상태
- 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속(managed) : 영속성 컨텍스트에 저장된 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed) : 삭제된 상태
비영속 (new)
단순히 엔티티를 새로 생성한 상태
Member member = new Member();
member.setId("member1");
member.setUsername("회원 1");
영속 (Managed)
새로 생성한 엔티티가 영속성 컨텍스트에 의해 관리되는 상태 저장 or 조회를 실행하면 영속성컨텍스트가 먼저 엔티티를 관리
영속성 컨텍스트(EntityManager)에 의해 관리된다는 뜻
EntityManager.persist(member); // 저장
EntityManager.find(id); //조회
준영속 (detach)
영속성 컨텍스트가 엔티티를 관리하지 않는 상태
Query가 commit을 호출해도 영속성을 거치지 않는 상태이기 때문에 동작하지 않음 1차캐시에 남아있지 않으므로 당연히 DirtyChecking 기능도 동작하지 않음 영속성컨텍스트의 모든 기능을 이용할 수 없는 상태
EntityManager.detach(member); // 준영속으로 만들기
EntityManager.clear() // 영속성 컨텍스트 초기화
삭제 (removed)
엔티티를 삭제한 상태
EntityManager.remove(member); // 삭제 명령
영속성 컨텍스트의 특징
- 영속성 컨텍스트와 식별자 값
영속성 컨텍스트는 엔티티 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분
영속 상태는 반드시 식별자 값이 반드시 있어야 함(식별자 값이 없으면 예외 발생)
- 영속성 컨텍스트와 데이터베이스 저장
JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영 = Flush
- 영속성 컨텍스트가 엔티티를 관리
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기
- 변경 감지
- 지연 로딩
엔티티 조회(영속)
캐시
영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이를 1차 캐시라 함
- 영속 상태의 엔티티는 모두 이곳에 저장
- 영속성 컨텍스트 내부에 Map이 하나 있는데 키는 @Id로 매핑한 식별자고 값은 엔티티 인스턴스
//엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원 1");
//엔티티 영속
em.persist(member);
//엔티티 조회
Member member = em.find(Member.class, "member1");
//EntityManager.find()의 메서드 정의
public <T> T find(Class<T> entityClass, Object primaryKey);
DB에서 데이터를 조회할 땐 무조건 1차캐시를 먼저 확인하고 없으면 1차캐시로 데이터를 영속화를 한 후에 값을 반환
장점
- 영속 엔티티의 동일성 보장(==)
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); // 동일성 비교 -> True 출력
- 데이터 값이 같은 두 객체는 참조하는 주소 값이 다름
- 하지만 EntityManager에 의해 관리되는 상태이면 두 객체는 같은 참조를 지님
※ 동일성과 동등성
동일성(identity)
실제 인스턴스가 같다. 참조값을 비교하는 == 비교의 값이 같다.
동등성(equality)
실제 인스턴스는 다를 수 있지만 인스턴스가 가지고 있는 값이 같음
자바에서는 동등성 비교는 equals() 메서드를 구현해야 함
엔티티 등록(비영속 -> 영속)
EntityManager em = emf.createEntityManager();
EntityManager transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경 시 트랜잭션을 시작
transaction.begin(); //트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않음
//커밋하는 순간 데이터베이스에 INSERT SQL을 보냄
transaction.commit(); //트랜잭션 커밋
- 모든 데이터 변경 Query를 쓰기지연 저장소에 보관해 놨다가 commit을 수행할 때 한번에 날림
@Slf4j
@SpringBootTest
@Transactional
@Rollback(false)
class UserTest {
@Autowired
EntityManager entityManager;
@Test
void lazy(){
log.info("===== persist 효출 전 =====");
User userA = new User("userA");
entityManager.persist(userA);
log.info("===== persist 효출 후 =====");
}
}
- persist 를 호출해도 바로 insert query가 발생하지 않고 commit시점에 호출이 된 것을 확인 가능
엔티티 수정(영속)
SQL 수정 쿼리의 문제점
SQL 사용 시 수정 쿼리를 직접 작성해야 함
UPDATE MEMBER
SET
NAME=?,
AGE=?,
GRADE=?
WHERE
id=?
- 수정 쿼리가 많이짐
- 비즈니스 로직을 분석하기 위해 SQL을 계속 확인해야 함
변경 감지(DirtyCheck)
엔티티의 변경사항을 데이터베이스에 자동으로 반영하는 기능
영속상태의 엔티티에만 적용 가능
- 트랜잭션 커밋하면 엔티티 매니저 내부에서 먼저 플러시(flush()) 가 호출
- 엔티티와 스냅샷을 비교해서 변경된 엔티티 찾기
- 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보내기
- 쓰기 지연 저장소의 SQL을 데이터베이스에 보내기
- 데이터베이스 트랜잭션을 커밋
※ 스냅샷
엔티티를 영속성 컨텍스트에 보관할 때, 최초의 상태를 복사해서 저장해 두는 것
수정 쿼리 작성
- JPA 기본 전략
- 엔티티의 모든 필드 업데이트(NAME, AGE, GRADE ... 등 모든 필드를 사용)
- 수정 쿼리가 항상 같아서(바인딩 되는 데이터는 다르지만) 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용 가능
- 데이터베이스에 동일한 쿼리를 보내면 데이터베이스는 이전에 한번 파싱된 쿼리를 재사용 가능
- 필드가 많거나 저장되는 내용이 너무 크면 수정된 데이터만 사용해서 동적으로 UPDATE SQL을 생성하는 전략
- @org.hibernate.annotations.DynamicUpdate
- 수정된 데이터만 사용해서 동적으로 UPDATE SQL을 생성(대부분 필드가 30개 이상일 경우)
- @DynamicInsert
- 데이터가 존재하는(null이 아닌) 필드만으로 INSERT SQL을 동적으로 생성
- @org.hibernate.annotations.DynamicUpdate
- 엔티티의 모든 필드 업데이트(NAME, AGE, GRADE ... 등 모든 필드를 사용)
@Slf4j
@SpringBootTest
@Transactional
@Rollback(false)
class UserTest {
@Autowired
EntityManager entityManager;
@Test
void dirtyChecking(){
User userA = new User("userA");
entityManager.persist(userA);
userA.setName("userB");
entityManager.flush();
entityManager.clear();
User userB = entityManager.find(User.class, userA.getId());
log.info("변경 후 = {}",userB);
}
}
엔티티 삭제(영속 -> 비영속)
Member memberA = em.find(Member.class, "memberA"); //삭제 대상 엔티티 조회
em.remove(memberA); //엔티티 삭제
- em.remove() : 삭제 대상 엔티티를 넘겨주면 엔티티 삭제
- 엔티티 등록과 비슷하게 삭제 쿼리를 쓰기 지연 SQL 저장소에 등록
- 트랜잭션을 커밋해서 플러시를 호출하면 실제 데이터 베이스에 삭제 쿼리 전달
- em.remove() 호출 순간 memberA는 영속성 컨텍스트에서 제거
※ Flush
영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것
- 변경 감지(Dirty Checking)가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해 수정된 엔티티를 발견
- 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록
- 쓰기지연 SQL 저장소의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제 쿼리)
- 동기화라 생각하면 편함
플러시 방법
- EntityManager.flush() 를 직접호출 (거의 사용 X)
- Transaction commit (자동호출)
- JPQL 쿼리를 직접 날림 (자동호출)
위 3가지 조건일 때 DB에 작업한 내용이 반영이 된다. ( 반영이 되어도 영속성 컨텍스트엔 해당 엔티티가 남아 있음)
플러시 모드 옵션
엔티티 매니저에 플러시 모드를 직접 지정시 javax.persistence.FlushModeType을 사용
- FlushModeType.AUTO
- 커밋이나 쿼리를 실행할 때 플러시(기본값)
- FlushModeType.COMMIT
- 커밋할 때만 플러시
준영속(영속 -> 준영속)
영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)된 것
준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없음
준영속 상태로 만드는 법
- em.detach(entity) : 특정 엔티티만 준영속 상태로 전환(해당 엔티티 관련 정보 1차 캐시, 쓰기지연 SQL 저장소에서 제거)
- public void detach(Ojbect entity);
- em.clear() : 영속성 컨텍스트를 완전히 초기화(영속성 컨텍스트를 제거하고 새로 만든것과 동일)
- em.close() : 영속성 컨텍스트를 종료
준영속 상태의 특징
- 거의 비영속 상태에 가까움
- 1차 캐시, 쓰기지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 어떠한 기능도 동작 X
- 식별자 값을 가지고 있음
- 준영속 상태는 이미 한 번 영속 상태였으므로 반드시 식별자 값을 가지고 있음(비영속과의 차이점)
- 지연 로딩 불가능
※ 지연 로딩
만약 연관관계에 있는 두 Entity 중(ex. user, team) 하나(user)를 호출 하였을때 그 와 연관된 다른 엔티티는 실제로 호출되기 전 까진 호출되지 않음
User만 호출 했을 경우 (user에 대한 select query 한번만 나감)
@Slf4j
@SpringBootTest
@Rollback(value = false)
@Transactional
class UserTest {
@Autowired
EntityManager entityManager;
Long userId;
@BeforeEach
void save(){
User userA = new User("userA");
Team team = new Team("team");
team.users.add(userA);
userA.setTeam(team);
entityManager.persist(team);
userId = userA.getId();
// 영속성 컨텍스트 초기화
entityManager.flush();
entityManager.clear();
}
@Test
@DisplayName("team is null")
void test(){
User user = entityManager.find(User.class, userId);
Team team = user.getTeam();
Assertions.assertNull(team.name);
}
}
User의 Team 까지 호출 하였을 경우 (프록시 강제초기화)
@Test
@DisplayName("team is not null")
void test(){
User user = entityManager.find(User.class, userId);
String teamName = user.getTeam().getName();
Assertions.assertNotNull(teamName);
}
team 의 getName() 을 호출해야만 team에 대한 select query가 한번 더 나감
즉, 실제로 사용하기 전 까진 query가 발생하지 않아 리소스 절감에 도움이 옵션을 사용하기 위해선 연관관계를 지정할때 FetchType=Lazy로 설정해야 동작함. (Default 옵션으로 Eager 이 설정되어 있어 query가 user만 호출해도 2번 발생함) 보통 @X To One 관계일때 Lazy로 걸어둠 ( 최적화 관련 옵션 )
병합 merge() (준영속 -> 영속)
준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환
public <T> T merge(T entity);
- merge()
- 파라미터로 넘어온 준영속 엔티티를 사용해서 새롭게 병합된 영속 상태의 엔티티를 반환
- 파라미터로 넘어온 엔티티는 병합 후에도 준영속 상태로 남아있음
- em.contains(entity)
- 영속성 컨텍스트가 파라미터로 넘어온 엔티티를 관리하는지 확인하는 메서드
- 즉, 아래에서 member는 준영속이므로 영속성 컨텍스트가 관리하지 않아서 false
- mergeMember는 영속이므로 영속성 컨텍스트가 관리해서 true
- 준영속인 member와 영속인 mergeMember는 서로 다른 인스턴스
동작 방법
- merge() 실행
- 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티 조회
- 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고 1차 캐시에 저장
- 조회한 영속 엔티티(여기선 mergeMember)에 member 엔티티의 값을 채워 넣는다
- member 엔티티의 모든 값을 mergeMember에 밀어 넣음
- 이때 mergeMember의 "회원1"이 "회원명변경"으로 바뀜
- mergeMember 반환
※ 비영속 merge
- 병합은 비영속 엔티티도 영속 상태로 만들 수 있음
Member member = new Member();
Member newMember = em.merge(member); //비영속 병합
tx.commit();
참고 자료
1. 김영한, 자바 ORM 표준 JPA 프로그래밍(출판지 : 에이콘, 2015)