JPA가 제공하는 기능은 크게 두 가지이다.
- 엔티티와 테이블을 매핑하는 설계 부분
- 엔티티를 실제로 사용하는 부분 → 3장에서 살펴볼 내용
데이터베이스를 하나만 생성하는 애플리케이션은 일반적으로
EntityManagerFactory
를 하나만 만든다.
/* 공장 만들기, 비용이 아주 많이 든다! */
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
/* 공장에서 엔티티 매니저 생성, 비용이 거의 안든다. */
EntityManager em = emf.createEntityManager();
🌱 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하다 → 공유 가능!
but, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생한다!
JPA를 이해하는 데 가장 중요한 용어는 영속성 컨텍스트다.
→ 굳이 우리말로 번역하자면 ‘엔티티를 영구 저장하는 환경’이다.
em.persist(member);
엔티티에는 4가지 상태가 존재한다.
- 비영속(
new/transient
) : 영속성 컨텍스트와 전혀 관계가 없는 상태- 영속(
managed
) : 영속성 컨텍스트에 저장된 상태- 준영속(
detached
) : 영속성 컨텍스트에 저장되었다가 분리된 상태- 삭제(
removed
) : 삭제된 상태
비영속
- 엔티티 객체를 생성했을 때, 순수한 상태로 아직 저장하지 않았다!
- 이는 영속성 컨텍스트나 데이터베이스와 전혀 관련이 없다. → 비영속 상태
영속
- 엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장했다.
- 이렇게 영속성 컨텍스트가 관리하는 엔티티 → 영속 상태
- = 영속성 컨텍스트에 의해 관리됨을 의미
준영속
- 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 관리하지 않으면 준영속 상태가 된다.
- 특정 엔티티를 준영속 상태로 만들려면? →
em.detach()
호출
- 혹은
em.close()
로 닫거나em.clear()
로 초기화해도 준영속 상태가 된다.
삭제
- 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다.
em.remove(member);
영속성 컨텍스트와 식별자 값
- 영속성 컨텍스트는 엔티티를 식별자 값(
@Id
로 테이블의 기본 키와 매핑한 값)으로 구분한다.
- 따라서 영속 상태는 식별자 값이 반드시 있어야 한다. → 없으면 예외 발생!
영속성 컨텍스트와 데이터베이스 저장
- 영속성 컨텍스트에 엔티티를 저장하면 언제 데이터베이스에 저장될까?
- → JPA는 보통 트랜잭션을 커밋하는 순간 반영한다.
- 이를 플러시(flush)라 한다.
영속성 컨텍스트가 엔티티를 관리하면 아래와 같은 장점이 있다.
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
- 지연 로딩
→ 영속성 컨텍스트가 왜 필요하고, 어떤 이점이 있는지 엔티티를 CRUD를 통해 알아보자!
영속성 컨텍스트는 1차 캐시라고 불리는 내부 캐시를 가진다.
- 영속성 상태의 엔티티는 모두 이곳에 저장된다.
- 쉽게 말하면 내부에 Map을 가지며, 이때 키는
@Id
로 매핑한 식별자이고 값은 엔티티 인스턴스이다.
//엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//엔티티를 영속
em.persist(member);
이번에는 엔티티를 조회해보자!
Member member = em.find(Member.class, "member1");
find()
메소드는 첫 번째 파라미터에 엔티티 클래스 타입을, 두 번째엔 식별자 값을 인수로 받는다.Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//1차 캐시에 저장됨
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
만약
em.find()
에서 1차 캐시에 엔티티가 없으면, 데이터베이스에서 엔티티를 조회한다.
이후, 1차 캐시에 저장한 후에 영속 상태의 엔티티를 반환한다.
Member findMember2 = em.find(Member.class, "member2");
Member a = em.find(Member.class, "member1");ㅅ
Member b = em.find(Member.class, "member1");
System.out.println(a == b); //동일성 비교 - 참!
em.find()
를 반복 호출해도 영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티를 반환한다.🌱 동일성과 동등성
동일성(identity) : 실제 인스턴스가 같다. 따라서 참조 값을 비교하는 == 비교의 값이 같다.
동등성(equality) : 실제 인스턴스는 다를 수 있지만, 인스턴스가 가진 값이 같다 (= equals())
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다.
transaction.begin(); //[트랜잭션]시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); //[트랜잭션] 커밋
엔티티 매니저는 커밋하기 전까지는 데이터베이스에 저장하지 않고, 내부 쿼리 저장소에 모아둔다.
이후, 커밋할 때 데이터베이스에 모아둔 쿼리를 보내는데 이를 트랜잭션을 지원하는 쓰기 지연이라 한다.
1차 캐시에 A 들어옴
2차 캐시에 B 들어옴
커밋 요청시 쓰기 지연 저장소에서 flush 후 Insert 쿼리 날림!
플러시(
flush
)란?
- 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업
- 등록, 수정, 삭제한 엔티티를 데이터베이스에 반영한다.
- = 쓰기 지연 SQL 저장소에 모인 쿼리를 데이터베이스에 보낸다.
- 이렇게 변경내용을 데이터베이스에 동기화를 한 후 트랜잭션을 커밋!
begin(); //tx start
save(A);
save(B);
save(C);
commit(); //tx commit
save()
를 호출할 때마다) 등록 쿼리를 데이터베이스에 보낸다.UPDATE MEMBER
SET
NAME=?,
AGE=?
WHERE
id=?
UPDATE MEMBER
SET
GRADE=?
WHERE
id=?
UPDATE MEMBER
SET
NAME=?,
AGE=?,
GRADE=?
WHERE
id=?
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); //start!
//영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
//영속 엔티티 데이터 수정
memberA.setUsername("leedongyeop");
memberA.setAge(25);
//em.update(member) -> 이런 코드가 필요하지 않을까?
transaction.commit(); //commit!s
JPA로 엔티티를 수정할 때 → 단순히 엔티티를 조회해서 데이터만 변경하면 된다!
em.update()
등의 작업 없이 데이터만 변경해도 데이터베이스에 자동으로 반영이 된다.
- → 이를 변경 감지(dirty checking)이라 한다.
스냅샷이란?
- JPA가 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는 것을 말한다.
- 주로 플러시 시점에 JPA가 엔티티와 비교하곤 한다.
변경 감지 과정
- 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 플러시(flush)가 호출된다.
- 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
- 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.
- 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
- 데이터베이스 트랜잭션을 커밋한다.
변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다.
- 비영속, 준영속처럼 관리를 받지 못하는 엔티티는 값을 변경해도 데이터베이스에 반영되지 않는다.
변경 감지로 인해 실행된 UPDATE SQL은 모든 필드를 업데이트 한다
UPDATE MEMBER
SET
NAME=?,
AGE=?
WHERE
id=?
UPDATE MEMBER
SET
NAME=?,
AGE=?,
GRADE=?
WHERE
id=?
만약 필드가 많거나 저장되는 내용이 너무 크다면? → 수정된 데이터만 사용하도록 동적 SQL 전략을 쓰자!
@Entity
**@org.hibernate.annotations.DynamicUpdate**
@Table(name = "Member")
public class Member {...}
💡 상황에 따라 다르지만, 컬럼이 대략 30개 이상이 되면 기본 방법(정적 수정 쿼리)보다 @DynamicUpdate
를 사용한 동적 수정 쿼리가 빠르다고 한다!
물론 가장 정확한 것은 본인의 환경에서 직접 테스트해보는 것이다!
단, 한 테이블에 컬럼이 30개 이상 된다는 것은 테이블 설계상 책임이 적절히 분리되지 않았을 수 있다.
//엔티티를 삭제하려면 먼저 삭제 대상 엔티티를 조회해야 한다.
Member memberA = em.find(Member.class, "memberA");
em.remove(memberA);
remove()
에 삭제 대상 엔티티를 넘겨주면 엔티티를 삭제한다.
- 물론 엔티티를 즉시 삭제하는 것이 아니라 삭제 쿼리를 쓰기 지연 SQL 저장소에 등록한다.
- 이후 트랜잭션을 커밋해 플러시를 호출하면 실제 데이터베이스에 삭제 쿼리를 전달한다!
💡 참고로 em.remove()
를 호출하는 순간 영속성 컨텍스트에서 제거된다.
이렇게 삭제된 엔티티는 재사용하지 말고 자연스럽게 GC의 대상이 되도록 두는 것이 좋다.
플러시(flush)는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
- 플러시를 실행하면 구체적으로 다음과 같은 일이 일어난다.
- 변경 감지가 동작해 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾는다.
- 이때 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다.
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.
영속성 컨텍스트에 플러시하는 방법은 3가지이다.
em.flush()
를 직접 호출하기- 트랜잭션 커밋 시 플러시가 자동 호출된다.
- JPQL 쿼리 실행 시 플러시가 자동 호출된다.
직접 호출
- 엔티티 매니저의
flush()
메소드를 직접 호출해서 영속성 컨텍스트를 강제로 플러시한다.- 테스트나 다른 프레임워크와 JPA를 함께 사용할 때를 제외하곤 거의 사용하지 않는다.
트랜잭션 커밋 시 플러시가 자동 호출
- 데이터베이스에 변경 내용을 SQL로 전달하지 않고 트랜잭션만 커밋하면 어떤 데이터도 반영되지 않는다.
- → 따라서 트랜잭션을 커밋하기 전에 꼭 플러시를 호출해 영속성 컨텍스트의 변경 내용을 반영해야 한다
- JPA는 이런 문제를 예방하기 위해 트랜잭션을 커밋할 때 플러시를 자동 호출한다.
JPQL 쿼리 실행 시 플러시 자동 호출
- JPQL이나 10장에서 설명할 Criteria 같은 객체지향 쿼리를 호출할 때도 플러시가 실행된다.
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();
엔티티 매니저에 플러시 모드를 직접 지정하려면
javax.persistence.FlushModeType
을 사용하자.
FlushModeType.AUTO
: 커밋이나 쿼리를 실행할 때 플러시한다. (디폴트)FlushModeType.COMMIT
: 커밋할 때만 플러시한다.
- 이 모드는 성능 최적화를 위해 사용할 수 있는데, 10장에서 알아보자.
💡 ***혹시라도 플러시로 영속성 컨텍스트에 보관된 엔티티를 지운다고 생각하지 말자!
→ 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화를 시키는 것임!
데이터베이스와 동기화를 최대한 늦추는 것이 가능한 이유는 트랜잭션이라는 작업 단위가 있기 때문
→ 트랜잭션 커밋 직전에만 변경 내용을 데이터베이스에 보내 동기화하면 된다.***
지금까지는 엔티티의
비영속 → 영속 → 삭제
상태 변화를 알아보았다.
이번엔영속 → 준영속
상태 변화를 알아보자.
준영속 상태 : 영속성 컨텍스트가 관리하는 영속 엔티티가 영속성 컨텍스트에서 분리된 것
- 따라서 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
영속성 상태의 엔티티를 준영속 상태로 만드는 방법 3가지
em.detach(entity)
: 특정 엔티티만 준영속 상태로 전환한다.em.clear()
: 영속성 컨텍스트를 완전히 초기화한다.em.close()
: 영속성 컨텍스트를 종료한다.
public void testDetached() {
/* 회원 엔티티 생성, 비영속 상태 */
Member member = new Member();
member.setId("memberA");
member.setUsername("회원A");
/* 회원 엔티티 영속 상태 */
em.persist(member);
/* 회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태 */
em.detach(member);
transaction.commit(); //트랜잭션 커밋
}
이렇게 영속 상태였다가 더는 영속성 컨텍스트가 관리하지 않는 상태를 준영속 상태라 한다.
- 더는 영속성 컨텍스트가 지원하는 어떤 기능도 동작하지 않는다!
em.clear()
는 영속성 컨텍스트를 초기화해서 해당 영속성 컨텍스트의 모든 엔티티를 준영속화한다.
//엔티티 조회, 영속 상태
Member member = em.find(Member.class, "memberA");
em.clear(); //영속성 컨텍스트 초기화
//준영속 상태
member.setUsername("changeName");
영속성 컨텍스트를 종료하면 해당 영속성 컨텍스트가 관리하던 엔티티가 모두 준영속이 된다.
public void closeEntityManager() {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("jpabook");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
Member memberA = em.find(Member.class, "memberA");
Member memberB = em.find(Member.class, "memberB");
transaction.commit();
em.close();
}
💡 영속성 상태의 엔티티는 주로 영속성 컨텍스트가 종료되면서 준영속 상태가 된다.
→ 개발자가 직접 준영속 상태로 만드는 일은 드물다!
거의 비영속 상태에 가깝다.
- 영속성 컨텍스트가 관리하지 않으므로 영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않는다.
- 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩 등등
식별자 값을 가지고 있다.
- 비영속 상태는 식별자 값이 없을 수도 있지만, 준영속 상태는 이미 한번 영속 상태였으므로 가진다.
지연 로딩을 할 수 없다.
- 지연 로딩이란?
- → 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법
- 하지만 준영속 상태는 영속성 컨텍스트가 관리하지 않으므로 지연 로딩 시 문제가 발생!
- 8장에서 알아보자.
준영속 상태의 엔티티를 다시 영속 상태로 변경하려면? → 병합을 사용하자!
merge()
: 준영속 상태의 엔티티를 받아 그 정보로 새로운 영속 상태의 엔티티를 반환
/* merge() 메소드 정의
public <T> T merge(T entity);
*/
Member mergeMember = em.merge(member);
public class ExamMergeMain {
static EntityManagerFactory emf =
Persistence.createEntityManagerFactory("jpabook");
public static void main(String args[]) {
Member member = createMember("memberA", "회원1");
member.setUsername("회원명변경"); //준영속 상태에서 변경
mergeMember(member);
}
static Member createMember(String id, String username) {
/* 영속성 컨텍스트1 시작 */
EntityManager1 em1 = emf.createEntityManager();
EntityTransaction tx1 = 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 void mergeMember(Member member) {
/* 영속성 컨텍스트2 시작 */
EntityManager 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());
//결과: false
System.out.println("em2 contains member = " + em2.contains(member));
//결과: true
System.out.println("em2 contains mergeMember = " + em2.contains(mergeMember));
em2.close();
}
}
병합은 비영속 엔티티도 영속 상태로 만들 수 있다!
Member member = new Member();
Member newMember = em.merge(member); //비영속 병합
tx.commit();