프록시
프록시 기초
- em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회. 쿼리 발생
- em.getReference()
: 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
: 쿼리는 안나가는데 객체 조회
: 껍데기는 똑같은데 안에는 텅텅 비어있음
: target은 진짜 레퍼런스를 가르킴(초기에는 null)
예제
em.find
Member findMember = em.find(Member.class, member.getId());
ddl
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.createdBy as createdB2_3_0_,
member0_.createdDate as createdD3_3_0_,
member0_.lastModifiedBy as lastModi4_3_0_,
member0_.lastModifiedDate as lastModi5_3_0_,
member0_.TEAM_ID as TEAM_ID7_3_0_,
member0_.USERNAME as USERNAME6_3_0_,
team1_.TEAM_ID as TEAM_ID1_7_1_,
team1_.createdBy as createdB2_7_1_,
team1_.createdDate as createdD3_7_1_,
team1_.lastModifiedBy as lastModi4_7_1_,
team1_.lastModifiedDate as lastModi5_7_1_,
team1_.name as name6_7_1_
from
Member member0_
left outer join
Team team1_
on member0_.TEAM_ID=team1_.TEAM_ID
where
member0_.MEMBER_ID=?
em.getReference
1.
Member findMember = em.getReference(Member.class, member.getId());
쿼리 안나감
2.
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
DDL
findMember.id = 1
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.createdBy as createdB2_3_0_,
member0_.createdDate as createdD3_3_0_,
member0_.lastModifiedBy as lastModi4_3_0_,
member0_.lastModifiedDate as lastModi5_3_0_,
member0_.TEAM_ID as TEAM_ID7_3_0_,
member0_.USERNAME as USERNAME6_3_0_,
team1_.TEAM_ID as TEAM_ID1_7_1_,
team1_.createdBy as createdB2_7_1_,
team1_.createdDate as createdD3_7_1_,
team1_.lastModifiedBy as lastModi4_7_1_,
team1_.lastModifiedDate as lastModi5_7_1_,
team1_.name as name6_7_1_
from
Member member0_
left outer join
Team team1_
on member0_.TEAM_ID=team1_.TEAM_ID
where
member0_.MEMBER_ID=?
findMember.username = hello
getReference를 호출하는 시점에는 query를 사용하지 않음
id를 가져오는 시점에도 query 할 필요 없음 -> id값을 파라미터로 넣어주었기 때문에 호출할 필요없음
userName은 db에 있으므로 getUserName()을 호출하는 시점에 db에 쿼리를 날림 => findMember에 값을 채운다음에 userName을 출력
3.
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
ddl
findMember = class hellojpa.Member$HibernateProxy$U7aJXjV7
=> findMember에 들어가 있는 것은 가짜 클래스 => 프록시 클래스
프록시 객체의 초기화
Member member = em.getReference(Member.class, “id1”);
member.getName();
Client에서 getName()을 호출하는 시나리오
1. Client에서 getName() 호출
2. target에는 아무것도 없음(null 값)
3. JPA가 영속성 컨텍스트에 진짜 Member 객체를 요청(초기화 요청)
4. 영속성 컨텍스트에서 DB 조회해서 실제 Entity 객체 생성
5. 프록시 객체의 멤버 변수인 target에 실제 Entity를 넣어줌
6. getName()을 했을 때 target의 getName()을 통해서 멤버의 getName() 반환
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.username = " + findMember.getUsername());
프록시 특징
- 프록시 객체는 실제 객체에 대한 참조를 보관한다. 그리고 프록시 객체의 메소드를 호출하면 프록시 객체가 실제 객체의 메소드를 호출한다.
:getName()을 호출하면 target에 있는 getName()을 대신 호출해준다.
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
- 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
( == 비교대신 instance of 를 사용해야 한다)
예제
더보기public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello"); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); try { Member member1 = new Member(); member1.setUsername("member1"); em.persist(member1); Member member2 = new Member(); member2.setUsername("member2"); em.persist(member2); em.flush(); em.clear(); Member m1 = em.find(Member.class, member1.getId()); Member m2 = em.getReference(Member.class, member2.getId()); logic(m1, m2); tx.commit(); } catch (Exception e){ tx.rollback(); } finally { em.close(); } emf.close(); } private static void logic(Member m1, Member m2) { System.out.println("m1 == m2: " + (m1.getClass() == m2.getClass())); }
m1 == m2 : false
public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello"); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); try { Member member1 = new Member(); member1.setUsername("member1"); em.persist(member1); Member member2 = new Member(); member2.setUsername("member2"); em.persist(member2); em.flush(); em.clear(); Member m1 = em.find(Member.class, member1.getId()); Member m2 = em.getReference(Member.class, member2.getId()); logic(m1, m2); tx.commit(); } catch (Exception e){ tx.rollback(); } finally { em.close(); } emf.close(); } private static void logic(Member m1, Member m2) { System.out.println("m1 == m2: " + (m1 instanceof Member)); System.out.println("m1 == m2: " + (m2 instanceof Member)); }
m1 == m2: true
m1 == m2: true - 사용하는 입장에서는 진짜 객체와 프록시 객체를 구분하지 않고 사용하면 된다.
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.
m1 = class hellojpa.MemberMember m1 = em.find(Member.class, member1.getId()); System.out.println("m1 = " + m1.getClass()); Member reference = em.getReference(Member.class, member1.getId()); System.out.println("reference = " + reference.getClass());
reference = class hellojpa.Member
: refrence에 실제 객체가 반환되는 이유
1. 이미 객체가 영속성 컨텍스트에 있기 때문에 원본 반환하는 것이 성능 최적화 관점에서 좋다.
2. JPA에서는 ==비교를 할 때 항상 TRUE가 나와야 한다.
한 영속성 컨텍스트에서 엔티티를 가져오고 PK가 똑같으면 그 엔티티들은 항상 ==비교에서 TRUE 여야한다.
따라서, ==비교에서 true가 나오도록 만들어주기 위해 실제 객체를 반환한다.
m1 == reference: trueSystem.out.println("m1 == reference: "+(m1 == reference));
cf)
refMember는 프록시가 반환되고 findMember도 프록시가 반환된다.Member refMember = em.getReference(Member.class, member1.getId()); System.out.println("m1 = " + refMember.getClass()); refMember.getUsername(); Member findMember = em.find(Member.class, member1.getId()); System.out.println("reference = " + findMember.getClass()); System.out.println("m1 == reference: "+(refMember == findMember));
따라서 refMember == findMember도 true가 나온다.
그 이유는 ==비교에서 true가 나오기 위해 proxy가 반환이 되면 em.find에서도 프록시가 반환된다. - ★초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 따라서 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다.
하이버네이트는 org.hibernate.LazyInitializationException 예외를 발생시킨다.
getUserName을 호출하면 영속성 컨텍스트가 없기 때문에 LazyInitializationException 예외 발생Member member1 = new Member(); member1.setUsername("member1"); em.persist(member1); em.flush(); em.clear(); Member refMember = em.getReference(Member.class, member1.getId()); System.out.println("regMember = " + refMember.getClass()); //proxy em.detach(refMember); //em.close(); //em.clear(); refMember.getUsername(); System.out.println("refMember = " + refMember.getUsername()); tx.commit();
프록시 확인
- 프록시 인스턴스의 초기화 여부 확인
emf.getPersistenceUnitUtil.isLoaded(Object entity) - 프록시 클래스 확인 방법
entity.getClass().getName() 출력(..javasist.. or HibernateProxy…) - 프록시 강제 초기화
org.hibernate.Hibernate.initialize(entity); - cf) JPA 표준은 강제 초기화 없음 강제 호출: member.getName()
즉시 로딩과 지연 로딩
- 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
- 설정 방법 : @ManyToOne(fetch = FetchType.EAGER)
- 지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회한다.
- 설정 방법 : @ManyToOne(getch = FetchType.LAZY)
즉시 로딩
@ManyToOne의 fetch 속성을 FetchType.EAGER로 지정한다.
// 즉시 로딩 실행 코드
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
find 할 때 조인을 이용하여 한방 쿼리로 member와 team을 가져온다.
따라서 team에는 프록시 객체가 아닌 진짜 객체가 들어가게 된다.
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.createdBy as createdB2_3_0_,
member0_.createdDate as createdD3_3_0_,
member0_.lastModifiedBy as lastModi4_3_0_,
member0_.lastModifiedDate as lastModi5_3_0_,
member0_.TEAM_ID as TEAM_ID7_3_0_,
member0_.USERNAME as USERNAME6_3_0_,
team1_.TEAM_ID as TEAM_ID1_7_1_,
team1_.createdBy as createdB2_7_1_,
team1_.createdDate as createdD3_7_1_,
team1_.lastModifiedBy as lastModi4_7_1_,
team1_.lastModifiedDate as lastModi5_7_1_,
team1_.name as name6_7_1_
from
Member member0_
left outer join
Team team1_
on member0_.TEAM_ID=team1_.TEAM_ID
where
member0_.MEMBER_ID=?
대부분의 JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다.
★프록시와 즉시로딩 주의
- 가급적 지연 로딩만 사용(특히 실무에서)
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
즉시 로딩이 10개가 더 걸려있다고 생각해보면 10개에 대해 조인 쿼리가 다 나가게 된다. - 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
sql문을 통해 member를 가져온다. member를 가지고 오니 team이 즉시로딩이 되어있으므로 team이란 값도 가지고 있어야 한다. member의 개수가 10개면 team을 가지고 오기 위해 10개에 대해 별도의 쿼리가 나간다.
select * from Member;
select * from Team where TEAM_ID =***
실제로는 위와 같은 쿼리가 나간다.
MEMBER에 2개의 데이터가 들어있을 때 JPQL문을 이용하여 MEMBER를 조회한다면 다음과 같은 SQL문이 나가게 된다.
/* select
m
from
Member m */ select
member0_.MEMBER_ID as MEMBER_I1_3_,
member0_.createdBy as createdB2_3_,
member0_.createdDate as createdD3_3_,
member0_.lastModifiedBy as lastModi4_3_,
member0_.lastModifiedDate as lastModi5_3_,
member0_.TEAM_ID as TEAM_ID7_3_,
member0_.USERNAME as USERNAME6_3_
from
Member member0_
Hibernate:
select
team0_.TEAM_ID as TEAM_ID1_7_0_,
team0_.createdBy as createdB2_7_0_,
team0_.createdDate as createdD3_7_0_,
team0_.lastModifiedBy as lastModi4_7_0_,
team0_.lastModifiedDate as lastModi5_7_0_,
team0_.name as name6_7_0_
from
Team team0_
where
team0_.TEAM_ID=?
Hibernate:
select
team0_.TEAM_ID as TEAM_ID1_7_0_,
team0_.createdBy as createdB2_7_0_,
team0_.createdDate as createdD3_7_0_,
team0_.lastModifiedBy as lastModi4_7_0_,
team0_.lastModifiedDate as lastModi5_7_0_,
team0_.name as name6_7_0_
from
Team team0_
where
team0_.TEAM_ID=?
- @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정
- @OneToMany, @ManyToMany는 기본이 지연 로딩
지연 로딩
@ManyToOne의 fetch 속성을 FetchType.LAZY로 지정한다.
// 지연 로딩 실행 코드
Member m = em.find(Member.class, member1.getId());
System.out.println("m = " + m.getTeam().getClass()); // 프록시 객체
team.getName(); // 팀 객체 실제 사용 -> 쿼리 호출
2번째 line에서 team은 프록시 객체이고, 3번째 line에서 team 객체를 실제 사용하므로 데이터베이스 조회가 이뤄진다.
영속성 전이와 고아객체
영속성 전이: CASCADE
- 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용
- 예: 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장.
주의
- 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함 을 제공할 뿐
CASCADE의 종류
고아 객체
- 고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티 를 자동으로 삭제
- orphanRemoval = true
- Parent parent1 = em.find(Parent.class, id); parent1.getChildren().remove(0); //자식 엔티티를 컬렉션에서 제거
- DELETE FROM CHILD WHERE ID=?
주의
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
- 참조하는 곳이 하나일 때 사용해야함!
- 특정 엔티티가 개인 소유할 때 사용
- @OneToOne, @OneToMany만 가능
- 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고 아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다. (delete 전파)
영속성 전이 + 고아 객체, 생명주기
CascadeType.ALL + orphanRemoval = true를 동시에 사용하면 어떻게 될까?