JPA - 프록시(Proxy)
Let’s learn about proxy
다음과 같이 회원 이름만 출력하는 코드가 있을 때 과연 멤버와 팀을 다 조회할 필요가 있을까? 조회해도 되지만 굳이 불필요하다는 걸 알 수 있다. 어떻게 개선할 수 있을까? 이를 위해 프록시(Proxy), 지연 로딩(Lazy Loading), 즉시 로딩(Eager Loading)에 대해 한번 알아보자.
public void printUser(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUseranme());
}
프록시(Proxy)
프록시는 대리(행위), 대리권, 대리인 등의 의미를 가지는데 여기서는 프록시 객체가 실제 객체의 참조를 보관하다 실제 사용할 때 실제 객체에 접근한다고 생각하면 된다.
프록시의 특징으로는
- 실제 클래스를 상속 받아서 만들어졌다.
- 실제 클래스와 겉 모양이 같다.
- 프록시 객체는 실제 객체의 참조(target)를 보관한다.
- 프록시 객체를 호출하면 프록시 객체는 실체 객체의 메소드 호출한다.
em.find() vs em.getReference()
em.find()는 데이터베이스를 통해서 실제 엔티티 객체를 조회해오고, em.getReference()는 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회해온다.
...
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember: " + findMember.getClass());
// -> findMember: Member 객체
...
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember: " + refMember.getClass());
// -> refMember: 프록시 객체(ex) Member$HibernateProxy$odcVHpjy)
프록시 객체 초기화 과정
Member member = em.getReference(Member.class, “id1”);
member.getName();
- getName() 호출 (처음에 Member target에 값이 없으면 2번 진행, 있으면 5번)
- JPA가 영속성 컨텍스트에 초기화 요청
- 영속성 컨텍스트가 DB를 조회한 후
- 실제 Entity를 생성해서 target에 연결
- target.getName()으로 진짜 Member의 getName을 접근해 이름을 반환
프록시의 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 처음 한 번만 초기화하면 target에 값이 채워져, target에 값이 있을 때부터는 바로 5번(초기화 과정 5번)을 실행한다.
- 즉, DB에 쿼리를 또 날리지 않는다.
- 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
- 프록시 객체를 초기화하고 getClass를 해봐도 그대로 프록시 객체임
- 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크 시 주의해야 함
- 원본 엔티티와 ==를 하면 비교 실패할 수가 있기 때문에 instance of를 사용
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해 도 실제 엔티티 반환
Member findMember = em.find(Member.class, member.getId()); System.out.println("findMember: " + findMember.getClass()); Member refMember = em.getReference(Member.class, member.getId()); System.out.println("refMember: " + refMember.getClass()); Systemout.println("findMember == refMember:" + (findMember == refMember)); // -> findMember: Member 객체 // -> refMember: Member 객체 // -> findMember == refMember: true
- 분명 em.getReference() 하면 프록시가 나온다 했는데 왜 실제 엔티티가 나왔을까?
- JPA에서는 같은 인스턴스의 ==에 대해 같은 영속성 컨텍스트 안에서 조회하면 항상 같다고 나옴.(이게 실제 객체든 프록시든 간에 상관없이 마치 자바 컬렉션에서 가져온 걸 == 비교하듯이)
- 이미 멤버를 영속성 컨텍스트(1차 캐시)에 올려놨는데 굳이 프록시로 받을 필요가 없어서(프록시로 받으면 오히려 손해)
- 그럼 반대로 em.getReference()를 먼저 하고 뒤에 em.find()를 하게 되면 어떻게 될까?
- 둘 다 프록시 객체가 반환되게 된다.
- 분명 em.getReference() 하면 프록시가 나온다 했는데 왜 실제 엔티티가 나왔을까?
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
Member refMember = em.getReference(Member.class, member.getId()); System.out.println("refMember: " + refMember.getClass()); // Proxy em.detach(refMember); //영속성 분리 refMember.getUsername(); //Exception 발생!
- 하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림
프록시 확인
- EntityManagerFactory로 부터 getPersistenceUnitUtil()
- 프록시 인스턴스의 초기화 여부 확인
- PersistenceUnitUtil.isLoaded(Object entity)
- 초기화 시 true
- 프록시 클래스 확인 방법
- entity.getClass().getName()
- ex) Entity$HibernateProxy$QPwAxVTy
- 프록시 강제 초기화
- org.hibernate.Hibernate.initialize(entity);
- 참고: JPA 표준은 강제 초기화 없음
- 강제 호출: member.getName()
근데 실제로는 getReference 이런 건 잘 쓰지 않는다. 하지만 이제 즉시 로딩과 지연 로딩에 대해 설명할 건데 앞의 프록시 매커니즘을 잘 알고 있어야 즉시 로딩, 지연 로딩에 대해 깊이 있게 이해할 수 있기 때문에 자세하게 설명했다. JPA가 제공하는 지연 로딩과 즉시 로딩에 대해 알아보자.
지연 로딩(Lazy Loading), 즉시 로딩(Eager Loading)
지연 로딩 (FetchType.LAZY)
단순히 member 정보만 사용하는 비즈니스 로직이 있다고 하면 Member를 조회할 때 Team도 함께 조회한다고 하면 손해이다. 그래서 JPA는 지연로딩이라는 Option을 제공한다.
@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;
..
}
위와 같이 FetchType.LAZY를 주게 되면 Member를 조회하더라도 Team은 프록시 객체로 조회하게 되어서 나중에 실제로 team을 사용하는 시점에 팀을 초기화(DB 조회) 할 수 있다.
즉시 로딩 (FetchType.EAGER)
하지만 Member와 Team을 자주 함께 사용한다고 하면? 즉시 로딩이라는 Option도 사용할 수 있다.
@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;
..
}
위와 같이 FetchType.EAGER를 주게 되면 Member 조회시 항상 Team도 조회하게 된다. JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회한다.
프록시와 즉시 로딩 주의
- 가급적 지연 로딩만 사용하자(특히 실무에서)
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
- 어떤 엔티티를 조회하면 즉시 로딩으로 연결된 엔티티를 다 Join 하기 때문에
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
- N+1: 최초 쿼리를 한 개 날렸는데 추가로 N 개가 나가는 것
- @ManyToOne, @OneToOne은 기본이 즉시 로딩
- LAZY로 설정해 주자
- @OneToMany, @ManyToMany는 기본이 지연 로딩
지연 로딩 활용 - 실무
- 모든 연관관계에 지연 로딩을 사용하자!
- 실무에서 즉시 로딩을 사용하지 말자!
- JPQL fetch 조인이나, 엔티티 그래프 기능을 사용하자!
참고: JPA
*틀린 부분이 있으면 언제든지 말씀해 주시면 공부해서 수정하겠습니다.