반응형
프록시
- Member와 Team이 연관관계에 있을 때, Member를 조회할 때 Team도 같이 조회해야 하는가?
- Member만 사용되는 경우에는 Team 까지 같이 조회한다면 불필요한 Join이 발생하여 비용이 추가로 발생한다.
- 반면에 Member와 Team이 모두 다 사용되는 경우에는 한 번 호출로 필요한 정보를 모두 얻어와서 추가적으로 호출을 보내지 않는다.
- JPA는 위와 같은 상황을 프록시를 이용해서 해결한다.
em.find()
- 데이터베이스를 통해서 실제 엔티티 객체 조회
em.getReference()
- 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
- 값이 실제로 사용되는 시점에 데이터베이스에 쿼리를 보낸다.
프록시 특징
- 실제 클래스를 상속 받아서 만들어 지며 실제 클래스와 겉 모양이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 알 수 없다. 구분하지 않고 사용하면 된다.
- 프록시 객체는 실제 객체의 참조(target)를 가지고 있다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 참조를 이용해서 메소드를 호출한다.
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화 할 때 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화 되면 프록시 객체 내부의 참조(target)가 실제 엔티티를 가리키며 이를 이용해 실제 엔티티의 값을 얻어온다.
- 프록시 객체는 원본 엔티티를 상속 받는다. 따라서 타입 체크 시 주의해야 한다.
- 타입 비교는 == 비교 대신에
instance of 키워드
로 비교해야 한다.
- 타입 비교는 == 비교 대신에
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면
em.getReference()
를 호출해도 실제 엔티티를 반환한다. 반대로em.getReference()
를 호출한 경우라면em.find()
를 호출해도 프록시 객체를 반환한다.- JPA에서는 같은 영속성 컨텍스트 안에서 동일한 Key라면 동일한 인스턴스임을 보장한다.
- 컬렉션 인스턴스에서 동일한 키를 꺼내는 것과 같다.
- 같은 영속성 컨텍스트에서 프록시 객체를 여러 개 사용 하더라도 동일한 프록시 객체를 사용한다.
- 같은 영속성 컨텍스트 안에서 동일한 인스턴스임을 보장하기 위함이다.
Member findMember1 = em.getReference(Member.class, 1L); Member findMember2 = em.getReference(Member.class, 2L); Member findMember3 = em.getReference(Member.class, 3L); System.out.println("findMember1 = " + findMember1.getClass()); System.out.println("findMember2 = " + findMember2.getClass()); System.out.println("findMember3 = " + findMember3.getClass()); [결과] findMember1 = class hellojpa.Member$HibernateProxy$Ku4ACJ8e findMember2 = class hellojpa.Member$HibernateProxy$Ku4ACJ8e findMember3 = class hellojpa.Member$HibernateProxy$Ku4ACJ8e
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생한다.
- 프록시 객체는 영속성 컨텍스트와 상호작용을 통해서 동작한다. 영속성 컨텍스트에서 관리되지 않으면
org.hibernate.LazyInitializationException
에러가 발생한다.
- 프록시 객체는 영속성 컨텍스트와 상호작용을 통해서 동작한다. 영속성 컨텍스트에서 관리되지 않으면
- 예) 프록시 객체 동작
Member member = em.getReference(Member.class, "id1");
member.getName();
- 프록시 객체 조회 시, 프록시가 모르는 값에 대해서 요청을 하면
- 프록시 객체는 영속성 컨텍스트에 초기화 요청을 한다.
- 영속성 컨텍스트는 데이터베이스를 조회해서
- 실제 엔티티 객체를 생성하여 프록시 객체의 target에 참조를 연결한다.
- 프록시 객체는 실제 엔티티의 참조를 이용해서 원래 요청을 수행한다.
프록시 확인 방법
PersistenceUnitUtil.isLoaded(Object entity)
: 프록시 인스턴스의 초기화 여부 확인
entity.getClass().getName()
: 프록시 클래스 확인
org.hibernate.Hibernate.initialize(entity)
: 프록시 강제 초기화- JPA 표준에는 강제 초기화가 없다.
- 강제 초기화를 지원하지 않으면 강제 호출 방식으로 프록시 초기화를 유도할 수 있다.
- 예)
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello"); EntityManager em = emf.createEntityManager(); Member findMember1 = em.getReference(Member.class, 1L); System.out.println("findMember1 = " + findMember1.getClass()); //entity.getClass().getName() boolean beforeLoaded = emf.getPersistenceUnitUtil().isLoaded(findMember1); System.out.println("before Loaded = " + beforeLoaded);// false Hibernate.initialize(findMember1); //프록시 강제초기화 //findMember1.getName(); // 강제 호출 boolean afterLoaded = emf.getPersistenceUnitUtil().isLoaded(findMember1); System.out.println("after Loaded = " + afterLoaded); //true
즉시 로딩과 지연 로딩
FetchType.LAZY
- 지연 로딩
- 프록시 객체로 조회한다.
- 실제 엔티티를 사용하는 시점에 프록시 객체가 초기화된다.
- 예)
@Entity @Getter @Setter public class Member { @Id @GeneratedValue private Long id; @Column(name = "USERNAME") private String name; @ManyToOne(fetch = FetchType.LAZY) // FetchType.LAZY으로 설정 @JoinColumn(name = "TEAM_ID") private Team team; } Team team = member.getTeam(); // 단순히 프록시 객체를 얻어옴 team.getName(); // 실제 엔티티를 사용하는 시점
FetchType.EAGER
- 즉시 로딩
- 실제 엔티티 객체로 조회한다.
- 엔티티를 조회 하는 시점에 연관된 엔티티를 조인을 사용해서 함께 가져온다.
- JPA 구현체에서 즉시 로딩을 구현하는 방법
- 조인을 사용해서 함께 연관된 데이터도 가져오는 방법
- 먼저 사용할 엔티티(
Member
)를 조회하고 이 엔티티의 연관관계 엔티티(Team
)가 즉시 로딩 설정되어 있다면 연관관계 엔티티(Team
)에 대한 조회 쿼리를 추가적으로 보내서 데이터를 가져오는 방법
프록시와 즉시로딩 주의점
- 실무에서는 모든 연관관계에 지연 로딩(
FetchType.LAZY
)을 사용해야 한다.- 즉시 로딩(
FetchType.EAGER
)을 사용하면 예상하지 못한 SQL이 발생된다
- 즉시 로딩(
- 즉시 로딩(
FetchType.EAGER
)은 JPQL에서N+1 문제
를 일으킨다.N+1 문제
란 최초 쿼리를 1번 날렸는데 추가적인 쿼리 N번이 발생하는 경우를 말한다.
- 즉시 로딩(
FetchType.EAGER
)은 데이터를 조회할 때 모든 값이 세팅되어 있어야 한다. 따라서 Member를 조회했을 때 Team이 즉시 로딩(FetchType.EAGER
)라면 Team의 값을 세팅하기 위해 추가적인 쿼리가 발생한다.
- 예)
Team teamA = new Team(); teamA.setName("TeamA"); em.persist(teamA); Team teamB = new Team(); teamB.setName("TeamB"); em.persist(teamB); Member member = new Member(); member.setName("aaa"); member.setTeam(teamA); em.persist(member); Member member2 = new Member(); member2.setName("bbb"); member2.setTeam(teamB); em.persist(member2); em.flush(); em.clear(); List<Member> members = em.createQuery("select m from Member m", Member.class) .getResultList(); [결과] /* select m from Member m */ //JPQL 쿼리 1번 호출 select member0_.id as id1_3_, member0_.USERNAME as USERNAME2_3_, member0_.TEAM_ID as TEAM_ID3_3_ from Member member0_ ========================================== select //Member에 해당하는 Team의 값을 세팅하기 위해 발생한 추가적인 쿼리 team0_.id as id1_5_0_, team0_.name as name2_5_0_ from Team team0_ where team0_.id=? ========================================== select //Member에 해당하는 Team의 값을 세팅하기 위해 발생한 추가적인 쿼리 team0_.id as id1_5_0_, team0_.name as name2_5_0_ from Team team0_ where team0_.id=?
@ManyToOne
,@OneToOne
은 기본 값이 즉시 로딩(FetchType.EAGER
)이다. 이를 지연 로딩(FetchType.LAZY
) 으로 설정해야 한다.
[참고자료]
자바 ORM 표준 JPA 프로그래밍 - 기본편, 김영한
반응형
'Java > ORM' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 값 타입 (0) | 2022.04.24 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 영속성 전이 (0) | 2022.04.19 |
[자바 ORM 표준 JPA 프로그래밍] 상속관계 매핑 (0) | 2022.04.06 |
[자바 ORM 표준 JPA 프로그래밍] 연관관계 매핑 (0) | 2022.04.03 |
[자바 ORM 표준 JPA 프로그래밍] 엔티티 매핑 (0) | 2022.03.31 |