프록시
- em.find(): DB에서 실제 엔티티 객체 조회
- em.getReference(): DB 조회를 미루는 프록시 엔티티 객체 조회
Member member = new Member();
member.setUsername("a");
em.persist(member);
em.flush(); // insert 문 실행
em.clear();
Member findMember = em.getReference(Member.class, member.getId()); // 프록시 Member class, select문 실행 X
- 프록시 특징
- 실제 엔티티를 상속받아 겉 모양이 같음
- 실제 객체의 참조를 보관
- 이론상 사용하는 입장에서 실제인지 프록시인지 구분 없이 사용하면 됨
- 프록시 객체 초기화
- 최초로 프록시 객체의 어떤 필드를 조회하면 DB를 통해 실제 객체를 생성해 프록시와 연결하는 작업 진행
- 이후에는 DB 조회 없이 프록시 객체에서 Getter 함수를 이용해 자유롭게 실제 엔티티에서 값 조회
Member member = em.getReference(Member.class, "id1");
member.getName();
Member member = new Member();
member.setUsername("a");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
String username = findMember.getUsername(); // 초기화 -> select 문 실행
Long id = findMember.getId(); // 이미 초기화 되었기 때문에 select 문 실행 X
- 프록시 객체 초기화 시, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님
- 프록시 객체는 원본 엔티티를 상속 받았기 때문에, 타입 체크시 == 비교 대신 instanceof 사용
- em.getReference()로 찾는 엔티티가 이미 영속성 컨텍스트에 있다면 실제 엔티티 반환
- JPA는 한 트랜잭션 안에서 == 비교시 true를 반환하도록 설계되었기 때문
- 실제와 프록시 구분없이 사용할 수 있게끔 설계된 것
// 둘다 실제 엔티티 반환
Member member = new Member();
member.setUsername("a");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId()); // 1차 캐시에 저장
System.out.println("findMember = " + findMember.getClass());
Member reference = em.getReference(Member.class, member.getId()); // 실제 엔티티 반환
System.out.println("reference = " + reference.getClass());
// 반대도 마찬가지
// 둘다 프록시 반환
Member member = new Member();
member.setUsername("a");
em.persist(member);
em.flush();
em.clear();
Member reference = em.getReference(Member.class, member.getId());
System.out.println("reference = " + reference.getClass());
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("findMember == reference = " + (findMember.getClass() == reference.getClass()));
- 영속성 컨텍스트의 관리에서 벗어난 준영속 상태일 때 프록시를 초기화하면 에러 발생
- 영속성 컨텍스트가 DB를 통해 실제 엔티티를 생성하여 연결해주지 못하기 때문
Member member = new Member();
member.setUsername("a");
em.persist(member);
em.flush();
em.clear();
Member reference = em.getReference(Member.class, member.getId()); // 프록시 객체 조회
System.out.println("reference = " + reference.getClass());
em.detach(reference); // 준영속 상태로 전환
//em.close();
//em.clear();
reference.getUsername(); // LazyInitializationException -> 초기화 불가
- 프록시 확인
- JPA 표준에는 강제 초기화가 없고 이는 hibernate가 지원하는 기능
Member reference = em.getReference(Member.class, member.getId());
System.out.println("reference = " + reference.getClass()); // 프록시 클래스 확인
System.out.println("isLoaded(reference) = " + emf.getPersistenceUnitUtil().isLoaded(reference)); // false
reference.getUsername(); // 강제 호출
Hibernate.initialize(reference); // 강제 초기화
System.out.println("isLoaded(reference) = " + emf.getPersistenceUnitUtil().isLoaded(reference)); // true
즉시 로딩과 지연 로딩(fetch)
- Member 조회 시 Team은 거의 조회하지 않는다면 -> 지연 로딩
// Member class
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
@JoinColumn
private Team team;
// JpaMain class
Team team = new Team();
team.setName("t");
em.persist(team);
Member member = new Member();
member.setUsername("a");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member m = em.find(Member.class, member.getId()); // 멤버 객체 조회 시 -> select 문 실행
System.out.println("m.getTeam().getClass() = " + m.getTeam().getClass()); // Team은 프록시 객체
m.getTeam().getName(); // 이때 프록시 객체 초기화 -> select 문 실행
- 대부분 같이 조회가 필요하다면 -> 즉시 로딩
// Member class
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
@JoinColumn
private Team team;
// JpaMain class
Member m = em.find(Member.class, member.getId()); // 멤버 객체 조회 시 -> join 후 select 문 실행
System.out.println("m.getTeam().getClass() = " + m.getTeam().getClass()); // Team은 실제 엔티티 객체
m.getTeam().getName(); // 그냥 실제 엔티티에서 getter 호출한 것 -> select 문 실행 X
- 가급적 지연 로딩(LAZY)만 사용! (필요할 때는 한번에 가져올 수 있는 방법들이 있음)
- 소규모가 아니라서 객체들끼리 복잡하기 얽혀있다면 조회마다 그것을 모두 조인하는 비효율적인 문제가 생김
- JPQL에서 N+1 문제를 일으킴: 1(최초 쿼리) + N(연관된 객체 조회 쿼리)
- 예) team1, team2 영속 -> member1, member2를 각각 team1, team2 소속으로 설정
- 'JPQL: select m from Member m' 실행: Member 객체 다 조회한 후 team1, team2 각각 다시 조회 (쿼리 3번)
- @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> 지연 로딩으로 설정
- @OneToMany, @ManyToMany는 기본이 지연 로딩
영속성 전이: CASCADE
- 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용
// Parent class
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) { // 연관관계 편의 메소드
childList.add(child);
child.setParent(this);
}
// Child class
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
// JpaMain class
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent); // parent만 영속했는데 자식 둘 모두 영속됨 -> insert 쿼리 세 번
- 영속성 전이는 연관관계 매핑과 아무 관련이 없음
- 종류: ALL, PERSIST, REMOVE, MERGE, REFRESH, DETACH
- Child 객체가 Parent 외 다른 객체와 연관되어있다면 cascade 사용 X (Parent 만이 Child를 관리한다면 사용)
- (+ 생애주기가 비슷할 때)
고아 객체
- orphanRemoval = true
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티(고아 객체) 자동 삭제 (delete 쿼리 실행됨)
- cascade와 마찬가지로 참조하는 곳이 하나일 때만 사용
Parent findParent = em.find((Parent.class), parent.getId());
findParent.getChildList().remove(0); // 컬렉션에서 제거(연관관계만 끊어짐) -> 해당 자식 엔티티 삭제
- orphanRemoval = true 일 때 부모 객체 삭제되면 자식 객체들도 모두 삭제됨 (CascadeType.REMOVE 와 동일효과)
- 참고: CascadeType.REMOVE는 부모 컬렉션에서 자식 제거해도 해당 자식 엔티티가 삭제되지 않음
영속성 전이 + 고아 객체, 생명주기
- CascadeType.ALL+ orphanRemoval = true 동시 사용 -> 자식의 생명주기 완전하게 관리
- 스스로 생명주기 관리하는 엔티티(Parent)는 persist로 영속, remove로 제거
- 이 엔티티가 관리하는 엔티티들(ChildList)은 관리자가 영속되면 따라 영속되고 제거되면 따라 제거됨
- 혹은 부모가 지정한 자식만 개별적으로 제거 가능
- 도메인 주도 설계(DDD)의 Aggregate Root 개념 구현시 유용
출처: 김영한님 JPA 프로그래밍 - 기본편
'course > inflearn' 카테고리의 다른 글
[JPA 프로그래밍 기본편] 객체지향 쿼리 언어 소개 (0) | 2022.09.19 |
---|---|
[JPA 프로그래밍 기본편] 값 타입 (0) | 2022.09.18 |
[JPA 프로그래밍 기본편] 고급 매핑 (0) | 2022.09.06 |
[JPA 프로그래밍 기본편] 다양한 연관관계 매핑 (0) | 2022.09.03 |
[JPA 프로그래밍 기본편] 연관관계 매핑 기초 (0) | 2022.09.01 |