JPA의 존재 목적이자 의의
- 객체 : 객체간에 멤버변수나 상속관계를 맺을 수 있다.
- RDB : 테이블들은 상속관계가 없고 모두 독립적으로 존재한다.
- 해결방법 : 매핑정보에 상속정보를 넣어준다. (
@OneToMany
,@ManyToOne
)
- 객체 : 참조를 통해 관계를 가지며 방향을 가진다. (다대다 관계도 있음)
- RDB : 외래키(FK)를 설정하여 Join 으로 조회시에만 참조가 가능하다. (즉, 다대다는 매핑 테이블 필요)
- 해결방법 : 매핑정보에 방향정보를 넣어준다. (
@JoinColumn
,@MappedBy
)
- 객체 : 참조를 통해 다른 객체로 순차적 탐색이 가능하며 콜렉션도 순회한다.
- RDB : 탐색시 참조하는 만큼 추가 쿼리나, Join 이 발생하여 비효율적이다.
- 해결방법 : 매핑/조회 정보로 참조탐색 시점을 관리한다.(
@FetchType
,fetchJoin()
)
- 객체 : 멤버 객체크기가 매우 클 수 있다.
- RDB : 기본 데이터 타입만 존재한다.
- 해결방법 : 크기가 큰 멤버 객체는 테이블을 분리하여 상속으로 처리한다. (
@embedded
)
- 객체 : 객체의 hashCode 또는 정의한 equals() 메소드를 통해 식별
- RDB : PK 로만 식별
- 해결방법 : PK 를 객체 Id로 설정하고 EntityManager는 해당 값으로 객체를 식별하여 관리 한다.(
@Id
,@GeneratedValue
)
객체와 데이터의 완벽한 매핑 불가로 인해 최적화 방법을 찾아야만 했음
1차 캐싱(영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소) + 2차 캐싱(애플리케이션 범위의 캐시로, 공유 캐시라고도 하며, 애플리케이션을 종료할 때 까지 캐시가 유지)
// Team.java
@Entity
@Cacheable
public class Team {
@Id @GeneratedValue
private Long id;
...
}
# application.yml
spring.jpa.properties.hibernate.cache.use_second_level_cache: true
# 2차 캐시 활성화합니다.
spring.jpa.properties.hibernate.cache.region.factory_class: XXX
# 2차 캐시를 처리할 클래스를 지정합니다.
spring.jpa.properties.hibernate.generate_statistics: true
# 하이버네이트가 여러 통계정보를 출력하게 해주는데 캐시 적용 여부를 확인할 수 있습니다.
# appplication.yml
spring.jpa.properties.javax.persistence.sharedCache.mode: ENABLE_SELECTIVE
객체의 영속성 상태는 Entity Manager 의 메소드를 통해 전환된다.
Raw JPA 관점에서 순서대로 요약정리 해보자면
- persist(),merge()
(영속성 컨텍스트에 저장된 상태, managed)
persist는 save, merge는 update라고 보기- flush()
(DB에 쿼리가 전송된 상태)
전송됐다고 다는 아니고...- commit()
(DB에 쿼리가 반영(발생)된 상태)
트랜잭션이 종료되는 시점에
Item item = new Item(); // 1️⃣
item.setItemNm("테스트 상품");
EntityManager em = entityManagerFactory.createEntityManager(); // 2️⃣
EntityTransaction transaction = em.getTransaction(); // 3️⃣
transaction.begin();
em.persist(item); // 4️⃣-1
em.flush(item); // 4️⃣-2 (DB에 SQL 보내기/commit시 자동수행되어 생략 가능함)
transaction.commit(); // 5️⃣
em.close(); // 6️⃣
1️⃣ 영속성 컨텍스트에 담을 상품 엔티티 생성
2️⃣ 엔티티 매니저 팩토리로부터 엔티티 매니저를 생성
3️⃣ 데이터 변경 시 무결성을 위해 트랜잭션 시작
4️⃣ 영속성 컨텍스트에 저장된 상태, 아직 DB에 INSERT SQL 보내기 전
5️⃣ 트랜잭션을 DB에 반영, 이 때 실제로 INSERT SQL 커밋 수행
6️⃣ 엔티티 매니저와 엔티티 매니저 팩토리 자원을 close() 호출로 반환
flush() 동작이 발생하기 전까지 최적화한다.
// 최적화 예시
update account set username = 'username' where id = 2;
update account set password = 'password' where id = 2;
// 이렇게 두 개 있지만 각각 날리지 않고 공통적으로 묶어 하나의 쿼리로 정리할 수 있는 것을...
update account set username = 'username', password = 'password' where id = 2;
// 엔티티 매니저가 이런 식으로 합쳐버림
flush() 동작으로 전송된 쿼리는 더이상 쿼리 최적화는 되지 않고, 이후 commit()으로 반영만 가능하다.
Team teamA = new Team();
teamA.setName("TeamA");
em.persist(teamA); // 영속성 상태화
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB); // 영속성 상태화
Member member_A = new Member();
member_A.setName("memberA");
member_A.setTeam(teamA);
em.persist(member_A); // 영속성 상태화
// 얘가 있냐 없냐에 따라서
em.flush();
// 결과 조회가 달라지게 된다
Member findMember = em.find(Member.class, member_A.getId());
Team findTeam= findMember.getTeam();
System.out.println(findTeam.getName());
// flush가 있는 경우
// 쓰기 지연: 트랜잭션을 커밋할 때 모아둔 쿼리를 반영하는 과정.
// flush: 이때 실제로 반영, 보내는 작업
// 쓰기 지연이 발생하지 않음
create member
create team
insert team // flush 작동 전까진 쓰기지연
insert member // flush 작동 전까진 쓰기지연
// flush()가 호출되기 전에는 영속성 컨텍스트의 변경 내용이
// 데이터베이스에 반영되지 않아 쓰기 지연이 발생
print "TeamA" (memberA.getTeam())
// flush() 호출 후에는 데이터베이스에 쿼리가 바로 반영
// DB에서 조회한 팀명을 갖고오게 됨
// flush가 없는 경우
// 트랜잭션이 끝날 때까지 insert 쿼리가 발생하지 않음
create member
create team
print "TeamA" (memberA.getTeam())
// 쓰기 지연이 발생하더라도 영속성 컨텍스트(1차 캐시)에서 조회해옴
insert team // 쓰기 지연이 발생한 부분
insert member // 쓰기 지연이 발생한 부분