JPA 3~4장 스터디
by ag-lee
3. 영속성 관리
JPA가 제공하는 기능
- 엔티티와 테이블을 매핑하는 설계 부분
- 매핑한 엔티티를 실제 사용하는 부분
엔티티 매니저
엔티티를 관리하는 관리자. 엔티티를 저장, 수정, 삭제, 조회 등 엔티티와 관련된 모든 일을 처리한다.
엔티티 매니저 팩토리와 엔티티 매니저
EntityManagerFactory
는 엔티티 매니저를 생성하는 공장인데, 공장을 만드는 비용은 매우 크므로 애플리케이션을 한 개만 만들어서 공유한다. 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하지만, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 절대 공유하면 안된다.
아래 코드로 엔티티 매니저 팩토리를 생성하면 __persistence.xml__을 바탕으로 생성한다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
EntityManager em = emf.createEntityManager();
EntityManager
는 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다. (Ex. 트랜잭션을 시작할 때) JPA 구현체들은 EntityManagerFactory
를 생성할 때 커넥션풀도 만든다.
영속성 컨텍스트
영속성 컨텍스트(persistence context)
는 엔티티를 영구 저장하는 환경으로 __엔티티 매니저로 엔티티를 저장, 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리__한다. 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어지고 엔티티 매니저를 통해 영속성 컨텍스트에 접근, 관리할 수 있다. 보통 하나의 엔티티 매니저에 하나의 영속성 컨텍스트가 만들어진다.
엔티티의 생명주기
비영속(new/transient)
: 영속성 컨텍스트와 전혀 관계가 없는 상태영속(managed)
: 영속성 컨텍스트에 저장된 상태준영속(detached)
: 영속성 컨텍스트에 저장되었다가 분리된 상태삭제(removed)
: 삭제된 상태
비영속
엔티티 객체를 처음 생성하면, 순수한 객체 상태이다. 이것을 비영속 상태라고 한다.
// 객체를 생성한 상태 (영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
영속
__영속성 컨텍스트가 관리하는 엔티티를 영속 상태__라고 한다.
// 객체를 저장한 상태(영속)
em.persist(member);
준영속
영속 상태의 __엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태__가 된다. em.detach()
를 호출하거나 em.close()
로 영속성 컨텍스트를 닫거나 em.clear()
로 영속성 컨텍스트를 초기화하면 엔티티는 준영속 상태가 된다.
삭제
엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다.
// 객체를 삭제한 상태(삭제)
em.remove(member);
영속성 컨텍스트의 특징
-
영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 primary key와 매핑한 값)으로 구분하므로 식별자 값이 반드시 있어야 한다.
- 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 저장한다.
- 영속성 컨텍스트가 엔티티를 관리할 때 장점
- 캐싱
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
- 지연로딩
엔티티 조회
영속성 컨텍스트는 내부에 __1차 캐시__를 가지고 있으므로 영속상태의 엔티티는 모두 캐싱된다. @Id
를 통해 식별한다. em.find()
를 호출하면 1차캐시를 찾고 없으면 데이터베이스를 조회한다.
영속 엔티티의 동일성 보장
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
a == b
: em.find로 같은 객체를 호출하면 1차캐시에 있는 같은 엔티티 인스턴스를 반환하므로 영속성 컨텍스트는 성능상 이점과 엔티티의 동일성을 보장한다.
JPA는 1차 캐시를 통해 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다는 장점이 있다.
엔티티 등록
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경 시 트랜젝션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT 하지 않는다.
// 커밋하는 순간 데이터베이스에 INSERT를 보낸다.
transaction.commit();
엔티티 매니저가 내부 쿼리 저장소에 Insert 문을 모아 두었다가 커밋할 때 트랜잭션을 데이터베이스에 보내는 것을 __쓰기지연__이라고 한다.
트랜잭션을 커밋하면 엔티티 매니저는 우선 영속성 컨텍스트를 __플러시__한다.
플러시
: 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 것.
엔티티 수정
memberA.setUsername("hi");
memberA.setAge(10);
//em.update(member) 가 필요하지 않을까?
변경감지(dirty checking)
: 엔티티를 영속성 컨텍스트에 보관할 때 최초 상태를 복사해서 저장해두는데 이것을 __스냅샷__이라고 하고, 플러시 시점에 스냅샷과 엔티티를 비교해 변경된 엔티티를 찾는다.
- 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 __플러시(flush())__가 호출된다.
- 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
- 변경된 엔티티가 있으면 수정쿼리를 생성해 SQL 저장소에 보낸다.
- SQL을 데이터베이스로 보낸다.
- 데이터베이스 트랜잭션을 커밋한다.
변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용한다.
JPA의 기본 전략은 엔티티의 모든 필드를 업데이트한다.
- 모든 필드를 사용하면 수정쿼리가 항상 같으므로 애플리케이션 로딩 시점에 쿼리를 생성해두고 재사용이 가능하다.
- 데이터베이스에 동일한 쿼리를 보내면 데이터베이스는 이전에 한번 파싱된 쿼리를 재사용할 수 있다.
엔티티 삭제
엔티티를 삭제하려면 삭제 대상 엔티티를 조회해야 한다. 삭제된 엔티티는 재사용하지 않고 GC 대상이 되도록 하는 것이 좋다.
Member memberA = em.find(Member.class, "memberA"); // 삭제 대상 엔티티 조회
em.remove(memberA); // 엔티티 삭제
플러시
플러시(flush())는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하다.
- 변경 감지가 동작해 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾는다. 수정된 엔티티는 수정 쿼리를 만들어 SQL 저장소에 등록한다.
- SQL 저장소의 쿼리를 데이터베이스에 전송한다. (등록, 수정, 삭제 쿼리)
영속성 컨텍스트를 플러시하는 방법
em.flush()
를 직접 호출한다.- 트랜잭션 커밋 시 플러시가 자동 호출된다.
- JPQL 쿼리 실행 시 플러시가 자동 호출된다.
플러시 모드 옵션
FlushModeType.AUTO
: 커밋이나 쿼리를 실행할 때 플러시 (기본 값)FlushModeType.COMMIT
: 커밋할 때만 플러시
준영속
준영속(detached)
는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것이다. 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
em.detach(entity)
: 특정 엔티티만 준영속 상태로 전환한다.- 이 메소드를 호출하면 영속성 컨텍스트에게 더는 엔티티를 관리하지 말라는 것이므로 1차 캐시, 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거된다.
em.clear()
: 영속성 컨텍스트를 완전히 초기화한다.em.close()
: 영속성 컨텍스트를 종료한다.
특징
-
거의 비영속 상태에 가깝다.
-
식별자 값을 가지고 있다.
-
지연 로딩을 할 수 없다.
- __지연 로딩__은 실제 객체 대신 프록시 객체를 로딩해두고 해당객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법이다.
병합: merge(entity)
준영속 상태의 엔티티를 받아서 그 정보로 __새로운 영속 상태의 엔티티를 반환__한다.
- merge()를 실행한다.
- 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다. 1차 캐시에 없으면 데이터베이스에서 조회한다.
- 조회한 영속 엔티티에 엔티티 값을 채워 넣는다.
- 반환한다.
비영속 병합
병합은 비영속 엔티티도 영속 상태로 만든다.
4. 엔티티 매핑
- 객체와 테이블 매핑:
@Entity
,@Table
- 기본 키 매핑:
@Id
- 필드와 컬럼 매핑:
@Column
- 연관 관계 매핑:
@ManyToOne
,@JoinColumn
@Entity
JPA를 사용해서 테이블과 매핑할 클래스는 @Entity
어노테이션을 필수로 붙여야한다.
- 기본 생성자는 필수다.
- final 클래스, enum, interface, inner 클래스에는 사용할 수 없다.
- 저장할 필드에 final을 사용하면 안 된다.
JPA가 엔티티 객체를 생성할 때 기본 생성자를 사용하므로 이 생성자는 반드시 있어야 한다.
@Table
엔티티와 매핑할 테이블을 지정한다. 생략시 매핑한 엔티티 이름을 테이블 이름으로 사용한다.
DDL 생성 기능
@Column(name = "NAME", nullable = false, length = 10) // 추가
private String username;
DDL을 생성할 때 해당 조건들을 통해 생성한다. 하지만, 단지 DDL을 자동 생성할 때만 사용되므로 JPA의 실행로직에는 영향을 주지 않는다. 때문에 직접 DDL을 만든다면 사용할 이유가 없다. 이 기능을 사용하면 애플리케이션 개발자가 엔티티만 보고도 제약조건을 파악할 수 있다.
기본키 매핑
- 직접할당 : 기본 키를 애플리케이션에서 직접 할당한다.
- 기본 키를 직접 할당하려면
@Id
만 사용하면된다.
- 기본 키를 직접 할당하려면
- 자동생성 : 대리 키 사용 방식.
@GeneratedValue
를 추가하고 원하는 키 생성 전략을 선택한다.- IDENTITY : 기본 키 생성을 데이터베이스에 위임한다.
- SEQUANCE : 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.
- TABLE : 키 생성 테이블을 만들어두고 시퀀스처럼 사용한다.
IDENTITY 전략
기본 키 생성을 데이터베이스에 위임하는 전략으로 주로 Mysql, postgreSQL 등에서 사용한다.
@GeneratedValue
의 strategy 속성 값을 GenerationType.IDENTITY로 지정하면 된다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
SEQUANCE 전략
@Id
@SequenceGenerator(
name = "BOARD_SEQ_GENERATOR",
sequenceName = "BOARD_SEQ", // 매핑할 데이터베이스 시퀀스 이름
initialValue = 1, allocationSize = 1)
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "BOARD_SEQ_GENERATOR")
private Long id;
}
@SequenceGenerator
로 시퀀스 생성기를 등록했다. sequenceName 속성의 이름으로 BOARD_SEQ를 지정하고 이를 @GeneratedValue
로 generator를 매핑한다.
TABLE 전략
키 생성 전용 테이블을 하나 만들고 이름과 같으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다.
AUTO 전략
선택한 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE 전략 중 하나를 자동으로 선택한다.
(오라클은 SEQUENCE, MySql은 IDENTITY를 사용)
필드와 컬럼 매핑 : 레퍼런스
@Column
객체 필드를 테이블 컬럼에 매핑
@Enumerated
enum 타입을 매핑할 때 사용.
- Value
- EnumType.ORDINAL : enum 순서를 데이터베이스에 저장. (기본)
- EnumType.STRING : enum 이름을 데이터베이스에 저장.
@Temporal
날짜 타입을 매핑할 때 사용.
@Lob
데이터베이스 BLOB, CLOB 타입과 매핑한다.
@Transient
이 필드는 매핑하지 않는다. 객체에 임시로 어떤 값을 보관하고 싶을 때 사용한다.
@Access
엔티티 데이터에 JPA가 접근하는 방식을 지정한다.
- 필드 접근: AccessType.FIELD로 지정. 필드에 직접 접근한다. private어도 접근 가능하다.
- 프로퍼티 접근 : AccessType.PROPERTY로 지정. Getter를 사용한다. (기본)
reference
자바 ORM 표준 JPA 프로그래밍 에이콘 오픈 소스 프로그래밍 시리즈,스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임워크