바로 시작~~!
Member 와 Team은 N:1 이다.
Member를 가져와야 할 때 Team의 데이터도 같이 가져온다.
물론 Team 테이블도 사용할 때는 좋지만 사용하지 않을때는 낭비지 않을까?
그래서 JPA에서는 이것을 프록시
, 지연로딩
으로 잡는다고 한다.
실제 엔티티 객체 대신에 사용되는 객체이다.
em.find() - 데이터베이스를 통해서 실제 엔티티 객체 조회.
em.getReference() - 데이터베이스 조회를 미루는 가짜(프록시)
엔티티 객체 조회.
DB에 쿼리가 안나가지만 객체는 주어지는 것.
Member findMember = em.find(Member.class, member.getId());
System.out.println(findMember.getId());
System.out.println(findMember.getUsername());
Member findMember = em.getReference(Member.class, member.getId());
System.out.println(findMember.getId());
System.out.println(findMember.getUsername());
아래 코드는 getReference() 하는 시점에서 쿼리를 쏘지 않는다.
이유는 getReference()를 사용하면 이 값이 사용되는 시점에서 쿼리를 쏘기 때문이다.
또한 두 번째 줄 .getId()에서도 쿼리를 쏘지 않는다.
이유는 첫 번째 줄에서 이미 파라미터로 getId()를 사용했기 때문이다.
여기서 findMember는 hibernate가 만든 가짜 클래스이다.
이것이 프록시 클래스이다.
1. 실제 클래스를 상속 받아서 만들어진다.
2. 실제 클래스와 겉 모양이 같다.
getId() 를 호출하면 실제 target에 있는 getId()를 대신 호출한다.
처음엔 사진과 같이 target이 비어있다.
그럼 어떻게 동작할까?????
프록시 객체인 member를 가져온다.
이 상황에서 getName()을 호출하면??????????
처음엔 아까 말했듯이 이 친구가 찾아갈 곳이 적혀있는 target이 null이다.
그렇게 되면 영속성 컨텍스트에 요청을 한다.
그 후 영속성 컨텍스트가 DB를 조회한 후 Member에게 Entity를 생성해준다.
생성 후 Proxy 안 targete이랑 진짜 Member랑 연결시켜준다.
3. 프록시 객체는 처음 사용할 때 한 번만 초기화
4. 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
5. 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)
6. 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
Member m1 = em.find(Member.class, memeber1.getId());
m1.getClass() 출력~
Member reference = em.getReferance(Member.class, memeber1.getId());
reference.getClass() 출력~
reference.getClass() 를 해주면 proxy객체가 나와야 할 거 같지만?? Member객체로 나온다.
이유는 이미 영속성 컨텍스트 안에 올라와있는데 굳이 proxy 할 이유도 없고, 성능도 원본 객체를 반환하는 게 좋기 때문?? 이다.
또 Jpa에서는 같은 영속성 컨텍스트 안이고, PK값이 같은 상태에서
a == a 를 하면 무조건 true가 되어야 한다.
그럼 그 반대는?
Member reference = em.getReferance(Member.class, memeber1.getId());
m1.getClass() 출력~
Member m1 = em.find(Member.class, memeber1.getId());
reference.getClass() 출력~
Jpa에서는 같은 영속성 컨텍스트 안이고, PK값이 같은 상태에서
a == a 를 하면 무조건 true가 되어야 한다. 라고 했다.
그럼 referance는 proxy객체니깐 m1도 proxy를 반환해준다. 신기방구..
7. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생(하이버네이트는org.hibernate.LazyInitializationException 예외를 터트림)
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember = " + refMember.getClass());
em.close(); //em.detach(refMember);
System.out.println("refMember = " + refMember.getUsername());
em.close()나 em.detach() 등 영속성 컨텍스트를 깨끗하게 해준 후 proxy를 가져오려고 하면 could not initialize proxy
에러가 발생한다.
실무에서 자주 접하는 에러라고 한다. 주의하자!!
emf.getPersistenceUnitUtil().isLoaded(refMembber)
.getUserName()으로 초기화 하기 전엔 false, 해준 후엔 true 반환
.getClass()
로 찍어본다.Hibernate.initialize(refMember);
• 참고: JPA 표준은 강제 초기화 없음
강제 호출: refMember.getName()
Member.java
(fetch = FetchType.LAZY)
team 을 프록시 객체로 줘야한다.
즉 맴버 클래스만 DB에서 조회를 한다.
예제를 보자.
Team team = new Team();
team.setName("temaA");
em.persist(team);
Member member = new Member();
member.setUsername("qwe");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member m = em.find(Member.class, member.getId());
System.out.println("refMember = " + m.getTeam().getClass());
System.out.println("=========");
m.getTeam().getName();
System.out.println("=========");
Memebr를 우선 가져오고, Team의 속성이 사욜될 때 프록시 객체가 초기화 되면서 DB에서 값을 가져온다.(Team에 대한 쿼리를 던진다.)
즉 이것이 지연로딩이다.
System.out.println("m = " + m.getTeam().getClass());
m은 프록시 객체이다.
만약 System.out.println("m = " + m.getClass());
라고 했다면
Member 객체일 것이다.
정리
Member와 Team이 지연로딩으로 Setting이 되어있다.
그렇게 되면 Member를 받아올 때 Team을 가짜 프록시 객체로 받아서 놓는다.
그 후 실제 team 속성을 사용하는 시점에 프록시가 초기화 되면서 쿼리가 나간다.
지연로딩은 Member와 Team이 연관관계가 있는데, Team을 거의 안쓸 때 사용했다.
하지만 Team도 같이 자주 쓰인다면?? 그럴 때 즉시로딩이 필요하다.
LAZY -> EAGER
Team team = new Team();
team.setName("temaA");
em.persist(team);
Member member = new Member();
member.setUsername("qwe");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member m = em.find(Member.class, member.getId());
System.out.println("m = " + m.getTeam().getClass());
System.out.println("=========");
m.getTeam().getName();
System.out.println("=========");
m.getTeam().getClass())
가 프록시 객체로 나왔던 LAZY와는 달리
Team 객체로 나오고, Member와 Team을 동시에 가져오는 것을 볼 수 있다.
이것이 즉시로딩이다.
정리
즉시로딩시, Member를 조회하면 Team도 같이 조회를 한다.
- 실무에서는 가급적 지연 로딩만 사용
왜? 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.- 즉시 로딩은 JPQL에서 N+1 문제를 발생시킨다.
- @ManyToOne, @OneToOne 은 기본이 즉시 로딩이다. 그래서 LAZY 설정을 해줘야 한다.
@OneToMany 는 기본값이 LAZY
cascade = CascadeType.ALL
이 선언되어 있다면,
parent 를 persist 할 때 그 밑에 있는 애들도 persist를 날려주는 것이다.
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.persist(child1);
em.persist(child2);
원래 세 번 persist를 해줘야 할 것을 cascade
해주면
em.persist(parent); parent만 persist 해줘도 DB에 저장이 된다.
영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
ALL
: 모두 적용
PERSIST
: 영속
이 두 가지를 많이 쓴다.
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 일컫는 말이다.
orphanRemoval=true
를 적용하고,
Parent findParent = em.find(Parent.class, parent.getId())
findParent.getChildList().remove(0)
하면 그 밑에 있는 데이터들도 다 삭제가 된다.
orphanRemoval
와 cascade=CascadeType.REMOVE
차이는 뭐지?부모 엔티티가 삭제되면 자식 엔티티도 전부 삭제되는 것은 동일하지만 원인이 다르다.
- orphanRemoval = true 옵션은 부모 엔티티가 사라지면서 자식 엔티티와의 참조(연결)가 끊어져서 삭제된다.
- CascadeType.REMOVE 옵션은 원래 엔티티가 삭제될 때 연관된 엔티티를 전부 삭제하는 옵션이다.
orphanRemoval 옵션은 Collections 에서 자식 엔티티를 삭제하는 걸로 DB 에서도 삭제 가능하지만 cascade 는 불가능
!주의할 점
- 참조하는 곳이 하나일 때 사용해야한다.
- 특정 엔티티가 개인 소유할 때 사용
- @OneToOne, @OneToMany만 가능
CascadeType.ALL + orphanRemovel=true
• 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화,
em.remove()로 제거
• 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명
주기를 관리할 수 있음
• 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때
유용
@ManyToOne, @OneToOne 의 기본은 즉시로딩!!!
(fetch = FetchType.LAZY) 해주자!!!
고아객체 삭제는 @OneToOne, @OneToMany만 가능!!!