프록시
- em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
- 프록시 객체를 반환
특징
- 프록시 객체는 실제 객체의 참조(target)를 보관
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제제 엔티티에 접근 가능
- 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함( == X, instanceof 사용)
m1.getClass() == m2.getClass() //false
m1 instanceof Member // true
m2 instanceof Member // true
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해 실제 엔티티 반환
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = " + m1.getClass()); //Member
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass()); //Member
m1 == reference //true
1차캐시에 이미 올라가 있기 때문에 프록시를 반환하지 않음
- 반대로 getReference()로 프록시객체를 가지고 있으면 실제로 find()를 했을때도 프록시 객체를 반환.
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember = "+ refMember.getClass());//Proxy
em.detach(refMember);
//em.clear
refMember.getUsername(); //org.hibernate.LazyInitializationException
프록시 객체의 초기화
Member member = em.getReference(Member.class, "id1");
member.getName();
1, getReference()를 호출하면 프록시객체를 가져온 다음, getName()을 호출하면 JPA가 영속성 컨텍스트에 초기화 요청을 한다.
2.영속성 컨텍스트에서는 실제 db를 조회해서 가져온 다음 실제 Entity에 값을 넣어 생성한 다음 프록시 객체는 실제 엔티티를 연결해서 실제 엔티티를 반환한다.
3. 그 이후에는 이미 초기화되어있는 프록시객체 이기 때문에 해당 엔티티를 반환한다.
프록시 확인
- 프록시 인스턴스의 초기화 여부 확인
- PersistenceUnitUtil.isLoaded(Objec entity)
- 프록시 클래스 확인 방법
- entity.getClass().getName() 출력(HibernateProxy)
- 프록시 강제 초기화
- org.hibernate.Hibernate,initialize(entity);
- 참고 : JPA 표준은 강제 초기화 없음
- 강제 호출:member.getName()
즉시 로딩과 지연 로딩
Member를 조회할 때 Team도 함께 조회해야 할까?
: 단순히 member 정보만 사용하는 비즈니스 로직
지연 로딩 LAZY를 사용해서 프록시로 조회(FetchType.Lazy)
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
Member m = em.find(Member.class, member1.getId()); //Member 객체 반환
System.out.println("m = " + m.getTeam().getClass(0)); //Team$HibernateProxy객체 반환
m.getTeam().getName() //team을 실제로 사용하는 시점에서 db조회 엔티티 반환
...
즉시로딩
Member와 Team을 자주 함께 사용한다면?
즉시 로딩 EAGER를 사용해서 함께 조회(FetchType.EAGER)
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.EAGER) //**
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
프록시와 즉시로딩 주의
- 가급적 지연 로딩만 사용(실무에서)
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
- 하나의 엔티티에 연관된 엔티티가 다수라면 finc() 한번에 수행되는 연관된 모든 테이블이 조회
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
/*Member*/
@Entity
public class Member{
...
@ManyToOne(fetch = FetchType.EAGER) //즉시로딩 사용
@JoinColumn(name="TEAM_ID")
private Team team;
...
}
...
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
//SQL: select * from Member
//SQL: select * from Team where TEAM_ID = xxx
...
- @ManyToOne, @OneToOne은 기본이 즉시 로딩 - >LAZY로 설정
N+1 해결 방안
1. 전부 지연로딩으로 설정한다.
fetch join을 사용해서 엔티티를 가지고 온다
List<Member> members = em.createQuery("select m from Member m fetch join m.team", Member.class)
.getResultList();
자연 로딩 활용
- 모든 연관관계에 지연 로딩을 사용
- 실무에서 즉시 로딩을 사용X
- JPQL fetch 조인이나, 엔티티 그래프 기능을 사용
- 즉시 로딩은 상상하지 못한 쿼리가 발생
영속성 전이 : CASCADE
- 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때
- 예 : 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장
영속성 전이 : 저장
persist 한 번으로 child 까지 같이 persist 가능?
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)
- 영속성 전이(CASCADE)를 이용한 엔티티 저장 방법
/*영속성 전이가 안되는 엔티티 저장 방빕*/
public class Parent{
....
@OneToMany(mappedBy = "parent", cascade=CascadeType.ALL) //영속성 전이
private List<Child> childList = new ArrayList<>();
public void addChild(Child child){
childList.add(child);
child,setParent(this);
}
...
}
@Entity
public class Child{
...
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
...
}
...
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);// parent만 persist 해주니 child도 같이 persist된다.
parent만 persist해주니 그에 관련된 childList들도 같이 persist가 된다.
CASCADE의 종류
- ALL : 모두 적용
- PERSIST : 영속
- REMOVE : 삭제
- MERGE : 병합
- REFRESH : REFRESH
- DETACH : DETACH
CASCADE - 주의사항
- 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐
고아 객체
- 고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
- orphanRemova = true
@Entity
public class Parent{
...
@OneToMany(mappedBy = "parent", cascade=CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child){
childList.add(child);
child.setParent(this);
}
...
}
@Entity
public class Child{
...
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
...
}
...
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);// parent만 persist 해주니 child도 같이 persist된다.
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0); // orphanRemoval 동작
주의사항
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
- 참조하는 곳이 하나일 때 사용해야함
- 특정 엔티티가 개인 소유할 때 사용
- @OneToOne, @OneToMany만 가능
- 참고 : 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.
- Parent객체를 지우게 되면 Parent가 소유하고있는 ChildList에 속한엔티티들이 전부 같이 삭제된다.
영속성 전이 + 고아 객체, 생명주기
- CascadeType.ALL + orphanRemovel=true
- 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
- 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
- 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현 할 때 유용
실전 예제 - 5.연관관계 관리
글로벌 페치 전략 설정
- 모든 연관관계를 지연 로딩으로
- @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연로딩으로 변경
영속성 전이 설정
- Order -> Delivery를 영속성 전이 ALL 설정
- Order -> OrderItem을 영속성 전이 ALL 설정
'개발 > JPA' 카테고리의 다른 글
값 타입 - 2 (0) | 2022.06.16 |
---|---|
값 타입 - 1 (0) | 2022.06.10 |
고급 매핑 (0) | 2022.06.07 |
다양한 연관관계 매핑 (0) | 2022.06.06 |
연관관계 매핑 기초 (0) | 2022.06.05 |