ㅇ 1차 캐시
ㅇ 동일성 보장
ㅇ 트랜잭션을 지원하는 쓰기 지연
ㅇ 변경 감지
ㅇ 지연 로딩
ㅇ 단방향
영속성 컨텍스트에서 persist된 값을 다시 조회할 경우 메모리에 있는 1차 캐시에서 엔티티를 조회한다.
- 영속성 컨텍스트 내부에 Map이 하나 있는데 키는 @Id로 매핑한 식별자이고, 값은 엔티티 인스턴스다.
- EntityManager.find()를 호출하면 먼저 1차 캐시에서 엔티티를 찾고 만약 찾는 엔티티가 1차 캐시에 없으면 데이터베이스를 조회한다.
- insert 쿼리 한 번만 나가고 find에 대한 쿼리는 나가지 않는 걸 확인할 수 있다.
@DisplayName("JPA 1차 캐시와 동일성 보장")
@Test
void jpaTest01(){
Member member = new Member("hyun",28);
entityManager.persist(member);
Member findMember1 = entityManager.find(Member.class, 1L);
Member findMember2 = entityManager.find(Member.class, 1L);
assertEquals(findMember1, findMember2); //true
}
- 결과
- 엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고
내부 쿼리 저장소에 INSERT SQL을 차곡차곡 모은다
- 그 후, 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스에 보내는데 이것을 트랜잭션을 지원하는 "쓰기 지연"이라한다.
- 쓰기 지연 SQL 저장소
member A를 영속화하면 1차 캐시에 저장하고 "쓰기 지연 SQL 저장소"에 보관한다.
- flush
- 영속성 컨텍스트의 변경 내용을 데이터 베이스에 동기화 하는 작업이다.
- 등록, 수정, 삭제한 엔티티를 데이터베이스에 반영한다.
- 이렇게 동기화한 후 커밋을 해야 데이터베이스에 반영된다.
- 트랜잭션을 지원하는 쓰기 지연과 JDBC 배치
insert(member1); //INSERT INTO...
insert(member2); //INSERT INTO...
insert(member3); //INSERT INTO...
insert(member4); //INSERT INTO...
insert(member5); //INSERT INTO...
commit();
-
네트워크 호출은 한 번은 단순한 메소드를 수만 번 호출하는 것보다 더 큰 비용이 든다.
- 위 코드는 5번의 INSERT SQL과 1번의 커밋으로 총 6번 데이터베이스와 토신한다.
- 이것을 최적화하려면 5번의 INSERT SQL을 모아서 한 번에 데이터베이스로 보내면 된다.
-
JPA 플러시 기능을 이용해 효과적으로 SQL 배치 기능을 사용할 수 있다.
- 최대 50건씩 모아서 배치를 실행한다.
- 같은 SQL 때만 유효하다.
hibernate.jdbc.batch_size = 50
- 이 경우는 1번(1,2,3,4), 2번(5), 3번(6,7)을 모아서 실행한다
em.persist(new Member()); // 1
em.persist(new Member()); // 2
em.persist(new Member()); // 3
em.persist(new Member()); // 4
em.persist(new Child()); // 5, 다른연산
em.persist(new Member()); // 6
em.persist(new Member()); // 7
- 데이터베이스 테이블 로우에 락 걸리는 시간 최소화(★)
update(memberA); // UPDATE SQL A
비즈니스로직A(); // UPDATE SQL ...
비즈니스로직B(); // INSERT SQL ...
commit();
- SQL을 사용할 경우
- UPDATE 시점부터 비즈니스로직A(), 비즈니스로직B()를 모두 실행하고 COMMIT 시점까지 해당 ROW에 락이 걸린다.
- UPDATE후 비즈니스로직A(), 비즈니스로직B()시점에도 락 걸려있음
- JPA의 경우
- COMMIT할 때 UPDATE SQL을 실행하고 데이터베이스 트랜잭션을 커밋하기 때문에 락 걸리는 시간을 최소화 할 수 있다.
- COMMIT 시점에만 UPDATE 하므로 비즈니스 로직 A,B 때는 락이 안 걸려있다.
- SQL 사용 시
- SQL 이용시 수정 쿼리가 많아지고, 비즈니스 로직을 분석하기 위해 SQL을 계속 확인해야한다.
- 결국 직접적이든 간접적이든 비즈니스 로직이 SQL에 의존하게 된다.
- 변경 감지(dirty-checking)
@DisplayName("변경 감지(더티체킹)")
@Test
void jpaTest02(){
Member member = new Member("hyun",28);
memberRepository.save(member);
member.setName("kim");
Member findMember = memberRepository.findByName("kim").get();
assertEquals(findMember.getAge(), member.getAge());
}
- 스냅샷
- JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는 데 이것을 스냅샷이라고 한다.
- 그리고 "플러시" 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.
-
변경 순서
- 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 플러시(flush())가 호출된다.
- 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
- 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.
- 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
- 데이터베이스 트랜잭션을 커밋한다.
-
특징
ㅇ 변경감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티만 적용된다.
ㅇ JPA의 기본 전략은 엔티티의 모든 필드를 업데이트한다.
-
JPA의 기본 전략은 엔티티의 모든 필드를 업데이트한다.
- 모든 필드를 사용하면 수정 쿼리가 항상 같다. 따라서 애플리케이션 로딩 시점에 수정쿼리를 미리 생성해두고 사용 가능
- 데이터베이스에 동일한 쿼리를 보내면 데이터베이스는 이전에 한 번 파싱된 쿼리를 재사용할 수 있다.
-
필드가 많거나 내용이 클 경우
- 하이버네이트 확장기능인 @DynamicUpdate를 사용해야한다.
-
※참고!!!
- 추천하는 방법은 기본 전략을 사용하고, 최적화가 필요할 정도로 느리면 그때 전략을 수정한다.
- 참고로 한 테이블에 컬럼이 30개 이상 된다는 것은 테이블 설계상 책임이 적절히 분리되지 않았을 가능성이 높다.
객체 그래프 탐색 등 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미루는 것
- 단방향 연관관계
ㅇ member.team 필드를 통해서 team을 알 수 있지만 team -> member는 알 수 없다.
- 양방향 연관관계
ㅇ 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고 반대로 팀과 회원도 조인할 수 있다.
ㅇ MEMBER 테이블의 TEAM_ID 외래키 하나로 MEMBER JOIN TEAM과 TEAM JOIN MEMBER 둘 다 가능하다.
- 예시
- 서로 MEMBER 테이블의 TEAM_ID 외래키 하나로 MEMBER JOIN TEAM과 TEAM JOIN MEMBER 둘 다 가능한 걸 확인할 수 있다.
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
SELECT *
FROM TEAM T
JOIN MEMBER M ON M.TEAM_ID = T.TEAM_ID
- 객체 연관관계 VS 테이블 연관관계
ㅇ 객체는 참조(주소)로 연관관계를 맺는다.
ㅇ 테이블은 외래 키로 연관관계를 맺는다.
※ 이 둘은 비슷해보이지만 매우 다른 특징을 가진다. 연관된 데이터를 조회할 때
객체는 참조(a.getB().getC())를 사용하지만 테이블은 조인 JOIN을 사용한다.
-
참조를 사용하는 객체의 연관관계는 단방향이다.
- A -> B (a.b)
-
외래키를 사용하는 테이블의 연관관계는 양방향이다.
- A JOIN B가 가능하면 반대로 B JOIN A도 가능하다.
-
객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야한다.
- A -> B (a.b)
- B -> A (b.a)
-
양방향 연관관계
@PostMapping("/bothwaysSave")
public void bothwaysSave(){
Team team1 = new Team("team1");
teamService.save(team1);
Member member1 = new Member("hyun",28,team1);
Member member2 = new Member("kim",28,team1);
memberService.save(member1);
memberService.save(member2);
team1.getMembers().add(member1);
team1.getMembers().add(member2);
List<Member> members = team1.getMembers();
for (Member member : members) {
System.out.println("member = " + member.getName());
}
}