본 포스팅은 인프런 - 자바 ORM 표준 JPA 프로그래밍 (기본편) 을 강의를 바탕으로 공부하고 정리한 글입니다.
프록시
JPA를 사용하면 객체 그래프를 통해 연관관계를 탐색할 수 있다는 장점이 있다.
그런데 엔티티들은 데이터베이스에 저장되어 있기 때문에 하나의 객체를 조회할 때 그와 연관되어 있는 엔티티들을 모두 조회하는 것보다는
필요한 연관관계만 조회해 오는 것이 효과적일 것이다.
예를 들어 Member와 Team이 있을 때, Member 조회 시 항상 Team의 정보도 함께 조회해야 할까?
실제 필요한 비즈니스 로직에 따라 다르다.
- Member와 Team을 거의 항상 같이 사용하는 경우
- Member와 Team을 거의 같이 사용하지 않고, Member만 출력하는 경우가 많으며 Team은 정말 어쩌다 호출하는 경우
비즈니스 로직 상 멤버의 username만 필요한데, 항상 팀에 대한 정보까지 같이 가져와서 사용하는 것은 낭비다.
JPA는 이러한 상황을 위해 프록시와 지연로딩이라는 개념을 사용한다.
프록시 기초
프록시는 "대신하다"라는 의미로, 동작을 대신해주는 가짜 객체이다.
- 하이버네이트는 지연 로딩을 구현하기 위해 프록시를 사용한다.
- 지연 로딩을 하려면 연관된 엔티티의 실제 데이터가 필요할 때 까지 조회를 미뤄야 하는데, 이때 연관관계로 가지고 있는 엔티티 필드에 null 값을 넣어둘 수는 없기 때문에 그 자리에 프록시 객체를 주입하여 실제 객체가 들어있는 것처럼 동작하도록 한다.
- 따라서 개발자는 연관관계 자리에 프록시 객체가 들어있든 실제 객체가 들어있든 신경쓰지 않고 사용할 수 있는 것이다.
JPA에서는 프록시 객체를 호출할 수 있는 em.getReference()라는 메서드를 제공한다.
- em.find() : 데이터 베이스를 통해서 실제 엔티티 객체를 조회
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회
💻 em.find()
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId()); // 실제 엔티티 객체 조회
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_.user_name as user_nam6_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.class() = class hellojpa.Member
em.find()로 멤버를 조회하면 데이터베이스에 조회 쿼리(select)가 나가고 실제 엔티티 객체가 조회된다.
💻 em.getReference()
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId()); // 프록시 엔티티 객체 조회
System.out.println("findMember.class() = " + findMember.getClass());
findMember.class() = class hellojpa.Member$HibernateProxy$gwS4XRep
그러나 em.getReference()로 멤버를 조회하면 조회 쿼리(select)가 나가지 않고 하이버네이트가 강제로 만든 프록시(가짜) 객체가 조회된다.
이 경우 실제로 정보가 필요한 시점에 데이터베이스에 쿼리가 나간다.
프록시 특징
- 프록시는 실제 클래스를 상속 받아 만들어진다.
- 실제 클래스와 겉 모양이 같기 때문에 프록시가 실제 객체처럼 동작할 수 있다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
- 프록시 객체는 실제 객체의 참조(target)를 보관한다.
- 프록시 객체의 메서드를 호출하면 프록시 객체를 통해 실제 객체의 메소드를 호출한다.
💡 프록시 객체가 실제 객체의 상속본인 것은 JPA 엔티티 생성의 다음 규칙을 만들어 냈다.
• 기본 생성자는 최소 protected 접근 제한자를 가져야 한다.
→ 기본 생성자가 private이면 프록시 생성시 super를 호출할 수 없음
• 엔티티 클래스는 final로 정의할 수 없다.
→ final로 정의할 경우 상속이 불가능
프록시 객체의 초기화
Member member = em.getReference(Member.class, "id1"); // 프록시 조회
member.getName; // 프록시 객체 초기화 (실제 조회 쿼리 나감)
- 최초 프록시 객체 조회 시점에는 프록시 내부 target에 참조값이 없다.
- 실제 객체의 메서드를 호출하는 시점에 target이 없다면, JPA는 영속성 컨텍스트에 프록시 객체 초기화를 요청한다.
- 영속성 컨텍스트는 초기화 요청을 받으면 데이터베이스에소 실제 엔티티 객체를 조회해오고, 이를 target에 연결시켜 실제 객체의 메소드를 호출한다.
- 프록시 객체에 target이 한번 할당된 이후에는, 프록시 객체 초기화를 안해도 된다.
정리
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다.
- 초기화되면 프록시 객체를 통해 실제 엔티티에 접근이 가능하게 되는 것이다.
- 정확히 말하자면 프록시 내부 target 값이 채워지는 것 뿐이다.
- 프록시 객체는 원본 엔티티를 상속 받는다. 프록시와 실제 객체의 타입이 다르기 때문에 타입 체크 시 주의가 필요하다.
- == 비교 실패, instance of 비교 성공
- 프록시 객체가 사용될 지 실제 객체가 사용될 지 모르기 때문에 JPA에서 타입 비교시 가능한 instance of를 사용하자
- 영속성 컨텍스트에 찾는 엔티티가 이미 있다면, em.getReference()를 호출해도 실제 엔티티를 반환한다.
반대로 처음에 프록시로 조회했다면, em.find()를 호출해도 프록시 엔티티를 반환한다.- JPA가 하나의 영속성 컨텍스트에서 조회하는 같은 엔티티의 동일성을 보장해줌
- 생각해보면, 이미 영속성 컨텍스트에 올려놓은 객체를 굳이 다시 프록시로 만들어 반환하는 것은 의미가 없다. 따라서 JPA는 그렇게 하지 않는다.
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시 초기화에 문제가 발생한다.
- 하이버네이트는 org.hibernate.LazyInitializationException 예외를 터뜨린다.
- 트랜잭션의 범위 밖에서 프록시 객체를 조회하려고 할 때 이 문제가 많이 발생한다.
- 이를 해결하기 위해 Spring Boot에서는 open-in-view 설정을 true로 가져가는데 이와 관련해서 더 자세한 내용은 이곳을 참고하자
프록시 확인 방법
프록시 확인을 도와주는 Util성 메서드를 제공해준다.
👉🏻 프록시 인스턴스의 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity)
👉🏻 프록시 클래스 확인 방법
member.getClass();
// 출력
class hellojpa.Member$HibernateProxy$sMHLOeTj
👉🏻 프록시 강제 초기화
org.hibernate.Hibernate.initialize(entity);
* 참고로 JPA 표준은 강제 초기화가 없다.
강제 호출 : member.getName()
Reference
'🌱 Spring > JPA' 카테고리의 다른 글
[JPA] JPA가 지원하는 쿼리 방법 (JPQL, Criteria, QueryDsl) (0) | 2022.10.29 |
---|---|
[JPA] 값 타입 (0) | 2022.10.19 |
[JPA] 영속성 전이(CASECADE)와 고아 객체 (0) | 2022.10.18 |
[JPA] 즉시 로딩(LAZY)과 지연 로딩(EAGER) (0) | 2022.10.18 |
[JPA] 상속관계 매핑(@Inheritance), 매핑 정보 상속@MappedSuperclass) (0) | 2022.10.17 |
[JPA] 연관관계 매핑(3) - 다중성 (ManyToOne, ManyToOne, OneToOne, ManyToMany) (1) | 2022.10.11 |
[JPA] 연관관계 매핑(2) : 양방향 매핑 (0) | 2022.10.06 |
[JPA] 연관관계 매핑(1) : 단방향 매핑 (0) | 2022.10.06 |