JPA가 제공하는 기능은 크게 2가지로 나눌 수 있다.
엔티티와 테이블을 매핑하는 설계부분
매핑한 엔티티를 실제로 사용하는 부분
매핑한 엔티티를 엔티티 매니저를 통해 어떻게 사용하는지 알아볼 것이다.
엔티티 매니저는 엔티티를 저장, 수정, 조회, 삭제하는 등 엔티티와 관련된 작업을 한다.
01. 엔티티 매니저 팩토리와 엔티티 매니저
DB를 하나만 사용하는 애플리케이션은 일반적으로 엔티티 매니저 팩토리를 하나만 생성한다.
다음은 엔티티 매니저 팩토리를 생성하는 코드이다.
// [엔티티 매니저 팩토리] 생성 (비용이 많이 듬)
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
Persistence.createEntityManagerFactory("jpabook")을 호출하면 META-INF/persistence.xml에 있는 정보를 바탕으로 엔티티 매니저 팩토리를 생성한다.
이제 필요할 때마다 엔티티 매니저 팩토리에서 엔티티 매니저를 생성하면 된다.
위 사진을 보자.
- 웹 어플리케이션에서 엔티티 매니저 팩토리는 엔티티 매니저를 생성한다.
- 엔티티 매니저 중 하나는 커넥션을 아직 사용하지 않았다.
- 엔티티 매니저는 트랜젝션이 시작되면 커넥션을 얻는다.
02. 영속성 컨텍스트란?
JPA를 이해하는 데에 가장 중요한 용어는 영속성 컨텍스트(persistence context)이다.
해석으로는 "엔티티를 영구 저장하는 환경"이라고 한다.
em.persist(member);
이 코드를 단순히 회원 엔티티를 저장한다고 표현했다.
정확히 이야기 하면 "회원 엔티티를 엔티티 매니저를 사용해서 영속성 컨텍스트에 저장한다." 라고할 수 있다.
03. 엔티티의 생명주기
- 비영속(new/transient) : 영속성 컨텍스트와 관계가 아예 없다.
- 영속(managed) : 영속성 컨텍스트에 저장된 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed) : 삭제된 상태
비영속
엔티티 객체를 생성했다.
지금은 순수 객체 상태이고, 아직 저장되지 않았다.
때문에 이 객체는 영속성 컨텍스트와 DB랑은 아무런 관계가 없다.
String id = "id1";
Member member = new Member();
member.setId(id);
member.setUsername("홍길동");
member.setAge(2);

영속
엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장했다.
이렇게 영속성 컨텍스트가 관리하는 엔티티의 상태를 영속 상태라고 한다.
즉, 영속 상태라는 것은 영속성 컨텍스트에 의해 관리된다는 뜻이다.
또한 em.find()나 JPQL을 사용해서 조회된 엔티티 또한 영속상태이다.
// 등록
em.persist(member);

준영속
영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 된다.
특정 엔티티를 준영속 상태로 만드려면 em.detach()를 호출하면 된다.
em.close()를 호출해서 영속성 컨텍스트를 닫거나,
em.clear()를 호출해서 영속성 컨텍스트를 초기화 해도 준영속 상태가 된다.
삭제
엔티티를 영속성 컨텍스트에서 삭제한다.
04. 영속성 컨텍스트의 특징
영속성 컨텍스트의 특징
1. 영속성 컨텍스트와 식별자 값
영속성 컨텍스트는 엔티티를 식별자값(@Id로 테이블과 기본 키와 매핑한 값)으로 구분한다.
따라서 영속 상태는 식별자값이 무조건 있어야 한다.
2. 영속성 컨텍스트와 DB 저장
영속성 컨텍스트에 엔티티를 저장하면 이 엔티티는 언제 DB에 저장될까?
JPA는 보통 트랜젝션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 DB에 반영하는데, 이것을 플러시(Flush)라고 한다.
3. 영속성 컨텍스트가 엔티티를 관리하면 생기는 장점
- 1차 캐시
- 동일성 보장
- 트랜젝션을 지원하는 쓰기 지연
- 변경 감지
- 지연 로딩
엔티티 조회
영속성 컨텍스트는 내부에 캐시를 가지고 있는데, 이를 1차 캐시라고 부른다.
영속 상태의 엔티티들은 모두 1차 캐시에 저장된다.
쉽게 말하면, 영속성 컨텍스트에 Map이 있다.
이 Map의 key값은 @Id로 매핑한 식별자이고, value는 엔티티 인스턴스이다.
String id = "id1";
Member member = new Member();
member.setId(id);
member.setUsername("홍길동");
member.setAge(2);
// 등록
em.persist(member);
이 코드를 실행하면 아래 그림처럼 1차 캐시에 회원 엔티티를 저장한다.
이 엔티티는 아직 플러시(Flush)가 되지 않았으므로 DB에는 저장되지 않았다.

1차 캐시의 key는 식별자 값이다.
그리고 식별자 값은 DB의 기본 키와 매핑되어있다.
따라서 영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 DB의 기본 키이다.
다음은 조회 예이다.
Member findMember = em.find(Member.class, "id2");
이 코드는 기본 키가 'id2'인 엔티티를 조회하는 코드이다.

조회하는 코드가 1차 캐시에 없으면 DB에서 접근한다.
아래 코드에서는 식별자가 'id1'인 엔티티를 조회한다.
Member findMember = em.find(Member.class, "id1");
1차 캐시에 'id1'인 key가 있으니까 value값인 Member1을 반환한다.

위 사진은 1차 캐시에 없는 값을 조회하는 과정이다.
영속 엔티티의 동일성 보장
다음 코드를 통해 식별자가 같은 엔티티 인스턴스를 비교해보자.
Member findMember1 = em.find(Member.class, "id1");
Member findMember2 = em.find(Member.class, "id1");
boolean isEqual = findMember1 == findMember2; // true
여기서 isEqual은 참일까 거짓일까?
em.find()을 호출할 때 식별자 값이 같다면 엔티티 매니저는 항상 같은 인스턴스를 반환한다.

엔티티 등록

다음은 엔티티를 등록하는 과정이다.
persist()를 호출하여 엔티티를 1차 캐시에 저장한다.
DB에 등록하는 SQL문을 저장소에 보관한다.
SQL 쿼리를 모아두는 것을 쓰기 지연 이라고 한다.
아직 DB에는 등록되어 있지 않은 상태이다.

앞서 트랜젝션들(등록 행위)을 커밋하는 것을 플러쉬 라고했다.
플러쉬를 하게 되면 모든 SQL쿼리가 DB로 전송되고, 엔티티들이 등록된다.
엔티티 수정
SQL 수정 쿼리의 문제점
SQL을 사용하면 수정 쿼리를 직접 작성해야 한다.
그런데 프로젝트가 점점 커지면서 수정 쿼리도 늘어나게 된다.
다음은 회원의 이름과 나이를 수정하는 SQL이다.
UPDATE MEMBER SET NAME = "이름1", AGE = 22 WHERE id = 'id1';
회원의 이름과 나이를 수정하는 기능을 만들었는데, 회원의 등급이 변경되는 기능이 추가되면 다른 수정 SQL을 추가로 작성한다.
결국 이렇게 되면 비즈니스 로직이 SQL에 의존적이게 된다.
변경 감지

- JPA는 엔티티를 엔티티 컨텍스트에 보관할 때, 최초 상태를 복사하여 저장하는데 이것을 스냅샷이라고 한다.
- 그리고 플러시 시점에서 엔티티와 스냅샷을 비교하여 변경된 엔티티를 찾는다.
- 변경된 엔티티가 있으면 해당 엔티티의 변경 내용을 UPDATE SQL로 작성한다.
- 쓰기 지연 저장소의 SQL을 DB에 전송한다.
- DB에서 트랜젝션을 커밋한다.
05. 플러시
플러시(flush)는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
플러시를 실행하면 다음과 같은 일이 일어난다.
- 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교하여 수정된 엔티티를 찾는다.
- 수정된 엔티티는 수정 쿼리를 만들고, 쓰기 지연 SQL저장소에 등록한다.
- 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송한다.(등록, 수정, 삭제 쿼리)
영속성 컨텍스트를 플러시하는 방법
1. em.flush()를 직접 호출한다.
- 엔티티 매니저의 flush() 메서드를 직접 호출해서 영속성 컨텍스트를 강제로 플러시한다.
- 테스트나 다른 프레임워크와 JPA를 함께 사용할 때르 제외하고 거의 사용하지 않는다.
2. 트랜젝션 커밋을 하면 자동으로 플러시가 호출된다.
- DB에 변경내용을 SQL로 전달하지 않고 트랜젝션만 커밋하면 어떤 데이터도 DB에 반영되지 않는다.
- 따라서 트랜젝션을 커밋하기 전에 꼭 플러시를 호출해서 영속성 컨텍스트 변경 내용을 DB에 반영해야 한다.
- JPA는 이런 문제를 예방하기 위해 트랜젝션을 커밋할 때 플러시를 자동으로 호출한다.
3. JPQL 쿼리 실행 시 자동으로 플러시가 호출된다.
플러시 모드 옵션
엔티티 매니저에 플러시 모드를 직접 지정하려면 javax.persistence.FlushModeType 을 사용하면 된다.
- FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러시(기본값)
- FlushModeType.COMMIT : 커밋할 때만 플러시
06. 준영속
영속 -> 준영속의 상태 변화를 알아보자
영속성 컨텍스트가 관리하는 영속상태의 엔티티가 영속성 컨텍스트에서 분리된 것을 준영속 상태라고 한다.
따라서 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
영속상태의 엔티티를 준영속 상태로 만드는 방법을 크게 3가지이다.
1. em.detach(entity) : 특정 엔티티만 준영속 상태로 전환한다.

1차 캐시에 특정 엔티티의 값을 삭제한다.
특정 엔티티와 관련된 SQL을 제거한다.
2. em.clear() : 영속성 컨텍스트를 완전히 초기화한다.

모든 엔티티를 1차 캐시에서 제거한다.
3. em.close() : 영속성 컨텍스트를 종료한다.

영속성 컨텍스트가 종료되어 더는 member1이 관리되지 않는다.
준영속 상태의 특징
그럼 준영속 상태인 회원 엔티티는 어떻게 되는 걸까?
1. 거의 비영속 상태에 가깝다.
영속성 컨텍스트가 관리하지 않으므로 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않는다.
2. 식별자 값을 가지고 있다.
비영속 상태는 식별자 값이 없을 수 있지만, 준영속 상태는 이미 한번 영속 상태였으므로 반드시 식별자 값을 가지고 있다.
3. 지연 로딩을 할 수 없다.
지연 로딩은 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 불러오는 방법이다.
병합 : merge()
준영속 상태의 엔티티를 다시 영속 상태로 변경하려면 병합을 사용하면 된다.
merge()메서드는 준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환한다.

Member mergeMember = em2.merge(member);
1. merge()를 실행한다.
2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
2-1. 만약 1차 캐시에 엔티티가 없으면 DB에서 엔티티를 조회하고, 1차 캐시에 저장한다.
3. 조회한 영속 엔티티(mergeMember)에 member 엔티티의 값을 채워 넣는다.
4. mergeMember를 반환한다.
병합이 끝나고, tx.commit을 호출해서 트랜잭션을 호출하면 어떻게 될까?
이름이 처음엔 "회원 1"에서 "회원명변경"으로 변경되었으므로 변경이 감지기능이 동작하여 변경 내용을 DB에 반영한다.
하지만 이렇게 구동하면 처음에 파라미터로 들어온 member은 아직 준영속 상태이다.
즉, 준영속 상태인 member 엔티티는 사용할 필요가 없다.
때문에 아래 코드로 변경해주는 것이 좋다.
member = em2.merge(member);
'JPA' 카테고리의 다른 글
JPA TIL - 06 다양한 연관관계 매핑 (0) | 2024.04.16 |
---|---|
JPA TIL - 05 연관관계 매핑 기초 (0) | 2024.04.09 |
JPA TIL - 04 엔티티 매핑 (0) | 2024.04.02 |
JPA TIL - 02 프로젝트 설정 및 엔티티 매니저 관리 (0) | 2024.03.25 |
JPA TIL - 01 JPA 1장 (0) | 2024.03.20 |