일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- builder
- Spring
- Volatile
- java
- middleware
- Dependency Injection
- nestjs
- OAuth 2.0
- 일급 객체
- 일급 컬렉션
- Google OAuth
- lombok
- factory
- synchronized
- spring security
- Today
- Total
HJW's IT Blog
JPA 연관관계에서 프록시 객체의 역할과 한계 본문
0. Hibernate 프록시 객체
본 글로 넘어가기전 간략하게 나마 Proxy 객체에 대해 짚고 넘어가겠다.
JPA 의 엔티티 간에 연관 관계가 있을때, fetch = FetchType.LAZY
가 설정되어 있다면, Hibernate 는 DB에 실제 쿼리를 날리는 대신 임시 프록시 객체를 반환한다. 이로 인해, Hibernate 는 데이터가 실제로 필요한 순간까지 SQL 을 실행하지 않을 수 있다.
Member member = em.getReference(Member.class, 1L);
이와 같이 getReference() 를 활용하면, 실제 클래스가 아닌 프록시 클래스를 조회할 수 있다.
1. 1:1 관계와 프록시 동작 분석
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@OneToOne(mappedBy = "member", fetch = FetchType.LAZY)
private MemberProfile profile;
}
@Entity
public class MemberProfile {
@Id
@GeneratedValue
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MEMBER_ID")
private Member member;
private boolean status;
}
이 관계는, MemberProfile 이 주인이며, FK를 관리하는 양방향 관계이다. 이때, DB에 Member 와 이 Member 를 참조하는 MemberProfile 을 INSERT 하고 영속성 컨텍스트를 비운뒤 다시 불러와서 Member.getProfile().getClass() 와 , MemberProfile.getMember().getClass() 를 실행하게 되면 어떻게 될까?
Member member = new Member();
MemberProfile memberProfile = new MemberProfile();
member.setProfile(memberProfile);
memberProfile.setMember(member);
em.persist(member);
em.persist(memberProfile);
tx.commit();
em.clear();
우선, 위와 같이 연관관계를 맺고, 확실하게 하기 위해 영속성 컨텍스트를 비우겠다.
MemberProfile findProfile = em.find(MemberProfile.class, 1L);
System.out.println("[MEMBER_PROFILE_GET_MEMBER] : " + findProfile.getMember().getClass());
이 결과는 예상대로 Proxy 객체를 반환한다.
[MEMBER_PROFILE_GET_MEMBER] : class hellojpa.Member$HibernateProxy$mRh3MsWK
그렇다면 그 역은 어떨까?
Member findMember = em.find(Member.class, 1L);
System.out.println("[MEMBER_GET_MEMBER_PROFILE] : " +findMember.getProfile().getClass());
[MEMBER_GET_MEMBER_PROFILE] : class hellojpa.MemberProfile
이 결과는 놀랍게도 Proxy 가 아닌, 실제 Member 객체이다. Hibernate 가 찍어주는 쿼리를 한번 살펴보자.
Hibernate:
select
m1_0.id,
m1_0.name
from
Member m1_0
where
m1_0.id=?
Hibernate:
select
mp1_0.id,
mp1_0.MEMBER_ID,
mp1_0.status
from
MemberProfile mp1_0
where
mp1_0.MEMBER_ID=?
첫번째 쿼리는 em.find(Member.class, 1L) 을 통해 생성된 것을 알겠다. 그렇다면 이 두번째 쿼리의 정채는 뭘까? 분명 FetchType.LAZY 를 설정했는데, 어째서 MemberProfile 를 조회하는 쿼리가 생성된 것일까?
1.1 프록시 객체의 생성 조건
일반적으로 Hibernate 는 fetch = FetchType.LAZY
일 경우, 프록시 객체를 반환하는 것이 일반적이다. 하지만 특정 조건에선, 개발자가 이와 같이 설정하였을 지라도, Eager Loading 을 하는 상황이 발생한다.
Hibernate 가 프록시 객체를 생성하기 위한 조건은 다음과 같다.
- FetchType.LAZY 설정 : (@OneToMany, @ManyToMany 는 기본적으로 LAZY 이다)
- 영속성 컨텍스트에 해당 엔티티가 없어야 한다
- 불러오려는 객체의 식별자(PK) 만 알고 있을때 이다.
그렇다면 위의 findMember.getProfile().getClass()
로 돌아가 조건들을 하나 하나 살펴보자. 1번 조건인 FetchType.LAZY 는 만족 되었다. 2번 조건인 영속성 컨텍스트에 해당 엔티티가 없어야 한다는 조건 또한 만족되었다. 하지만, Hibernate 가 프록시를 생성할 수 없는 이유는 3번쨰 조건에 있다. Hibernate 는 연관관계에 있어 주인이 아닌 엔티티의 연관 엔티티의 식별자를 모른다.
여기서 가장 중요한 개념은 객체 그래프 탐색 과 DB 관계와 Java 관계의 차이 이다.
DB 의 관계
- DB 에서 양방향 관계는 단순히 Foreign Key 하나로 해결된다.
- MemberProfile 테이블에는
MEMBER_ID
FK 가 존재한다 - 즉, MemberProfile -> Member 방향으로 쉽게 접근할 수 있다
- 반대로, Member -> MemberProfile 또한 MemberID로 쉽게 접근할 수 있다.
JAVA 의 관계
- JAVA 에선 객체 참조로 관계를 맺는다.
- MemberProfile 이 연관 관계의 주인이므로, 실제 FK 를 관리한다 (MEMBER_ID)
- 즉, MemberProfile 은 Member 의 식별자를 알고 있다
- 하지만, Member 는 MemberProfile 의 식별자를 직접 알지 못한다.
- 그렇기에, Member 에서 MemberProfile 을 찾으려면, Hibernate 는 추가 조회를 해야만 한다.
1.2 반대의 상황은 어떨까
자연스럽진 못하지만 만약, Member 가 관계의 주인이고 MemberProfile_ID 를 직접 관리하고, MemberProfile 이 Member_ID를 알지 못하는 상황이라면 어떨까?
- Member 를 조회했을때, MemberProfile 프록시 객체를 만들 수 있다.
- 반대로 MemberProfile 을 조회했을때, Hibernate 는 Member 프록시를 생성하지 못하고 추가 쿼리를 날려 실제 Member 를 조회한다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MEMBER_PROFILE_ID")
private MemberProfile profile;
}
@Entity
public class MemberProfile {
@Id
@GeneratedValue private Long id;
@OneToOne(mappedBy = "profile",fetch = FetchType.LAZY)
private Member member;
private boolean status;
}
[MEMBER_Profile] : class hellojpa.MemberProfile$HibernateProxy$pg6OTFcX // Member 에서 조회한 profile
Hibernate:
select
mp1_0.id,
mp1_0.status
from
MemberProfile mp1_0
where
mp1_0.id=?
Hibernate:
select
m1_0.id,
m1_0.name,
m1_0.MEMBER_PROFILE_ID
from
Member m1_0
where
m1_0.MEMBER_PROFILE_ID=?
[Profile_MEMBERS] : class hellojpa.Member // profile 에서 조회한 member
추가 SELECT 문이 날라가는것을 볼 수 있다.
2. 1:N, N:1 에서의 동작
이제 1:N 에서의 동작을 살펴보자.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@OneToMany(mappedBy = "member")
private List<Orders> orders = new ArrayList<>();
}
@Entity
public class Orders {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MEMBER_ID")
private Member member;
}
이 관계에서 주인은 Orders 이며, FK 또한 Orders 가 관리한다.
Member member = new Member();
Orders order1 = new Orders();
Orders order2 = new Orders();
member.getOrders().add(order1);
member.getOrders().add(order2);
order1.setMember(member);
order2.setMember(member);
em.persist(member);
em.persist(order1);
em.persist(order2);
이와 같이 DB 에 삽입하고, clear 후 다시 불러와 보겠다. 이전 1:1 의 결과에 따르면,
Member findMember = em.find(Member.class, 1L);
System.out.println("[MEMBER_ORDER] : " + findMember.getOrders().getClass());
호출시, 프록시가 아닌 실제 객체들이 들어 있어야 한다.
[MEMBER_ORDER] : class org.hibernate.collection.spi.PersistentBag
하지만 이상한게 들어있다. PersistentBag
는 또 뭘까?
PersistentBag
는 Hibernate 에서 Colleciton 타입의 필드를 관리하는 내부 구현체이다. 이때 이는 단순한ArrayList
가 아닌 영속성 컨텍스트와 연결된 특별한 컬랙션이 된다.- 이
PersistentBag
를 통해, Hibernate 는 실제 대상 엔티티들의 식별자를 모르더라도 프록시를 생성할 수 있다. - 이후 Collection 의 요소에 접근하는 순간, Hibernate 는 추가 쿼리를 날려 데이터를 조회한다.
Orders findOrder1 = em.find(Orders.class, 1L);
System.out.println("[ORDER_MEMBER] : " + findOrder1.getMember().getClass());
이 경우는, 1:1 과 동일하다. Orders 객체는 Member 의 FK 를 관리하는 주인이므로 Member 의 식별자를 알고 있다. 즉, Proxy 객체를 만들 수 있다.
[ORDER_MEMBER] : class hellojpa.Member$HibernateProxy$mEh3Lhzc
3. N:M 에서의 동작
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "MEMBER_PRODUCT",
joinColumns = @JoinColumn(name = "MEMBER_ID"),
inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID")
)
private List<Product> products = new ArrayList<>();
}
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
@ManyToMany(mappedBy = "products", fetch = FetchType.LAZY)
private List<Member> members = new ArrayList<>();
}
마지막으로 N:M 에서의 동작에 대해 짚고 넘어가겠다. 이전 내용들을 이해했다면, 이번 동작은 매우 간단하다.
Member member = new Member();
member.setName("member");
Product product1 = new Product();
Product product2 = new Product();
member.getProducts().add(product1);
member.getProducts().add(product2);
product1.getMembers().add(member);
product2.getMembers().add(member);
em.persist(member);
em.persist(product1);
em.persist(product2);
이와 같이 세팅을 해주고, 각각 member.getProducts.getClass
와 product.getMembers.getClass
를 실행하면
Member findMember = em.find(Member.class, 1L);
System.out.println("[MEMBER_PRODUCTS] : " + findMember.getProducts().getClass());
em.clear();
Product findProduct = em.find(Product.class, 1L);
System.out.println("[PRODUCT_MEMBERS] : " + findProduct.getMembers().getClass());
[MEMBER_PRODUCTS] : class org.hibernate.collection.spi.PersistentBag
[PRODUCT_MEMBERS] : class org.hibernate.collection.spi.PersistentBag
둘 모두 Collection 을 다루기에 이전에 설명한 PersistentBag 를 사용한 프록시 객체를 생성한 것을 볼 수 있다.
'JAVA' 카테고리의 다른 글
JPA - Entity들 사이의 연관관계 (0) | 2025.02.25 |
---|---|
JPA - EntityManager와 영속성 컨텍스트 이해하기 (0) | 2025.02.25 |
DTO ↔ 엔티티 변환, MapStruct로 자동화하기 (0) | 2025.02.18 |
Spring 없이 의존성 관리와 팩토리 패턴 구현하기 (0) | 2025.01.24 |
JVM 은 어떻게 동작하는가 (1) | 2025.01.24 |