BackEnd
[JPA] Proxy
Mike야
2025. 1. 20. 21:42
새로운 기술을 배울 떄, 우린 이 기술이 왜 필요한지에 대한 고민을 해야합니다.
다음과 같은 두 객체가 있는 상황을 가정해 볼게요.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
}
멤버를 조회해 볼까요?
Member member = em.find(Member.class, 1L);
Team team = member.getTeam();
제가 원하는 정보가 name이였다고 할 때, team에대한 정보가 굳이 필요할까요?
불필요한 데이터 로딩은 성능에 영향을 미치지 않을까요?
이런 불필요한 데이터의 로딩은 성능에 악 영향을 줄 것이고, 이러한 문제를 해결하기 위해 proxy 기술을 사용합니다.
이런 proxy 기술을 단계별로 설명해 보자면 다음과 같습니다.
jpa 는 자체적으로 member를 상속하는 proxyMemeber 클래스를 만듭니다.
// 실제 엔티티
@Entity
public class Member {
private Long id;
private String name;
}
// 프록시 클래스 (Hibernate가 자동 생성)
public class MemberProxy extends Member {
private Member target; // 실제 엔티티 참조
private boolean initialized = false; // 초기화 여부
public String getName() {
if (!initialized) {
initializeTarget(); // 실제 데이터 로딩
initialized = true;
}
return target.getName(); // 실제 엔티티의 메소드 호출
}
}
여기서 EntitiyManager 의 메서드인 getReference() 를 사용하면 다음과 같은 일이 발생합니다.
Member proxyMember = em.getReference(Member.class, 1L);
// 이 시점에는:
// - DB 조회 안함
// - 프록시 객체만 생성
// - target = null
이렇게 초기화되기 전 proxy 객체가 존재하고, 특정 상황에서 실제 데이터가 필요해졌다고 생각해봅시다.
이떄 내부적으로 다음 단계가 진행됩니다.
// 실제 데이터가 필요한 시점
String name = proxyMember.getName();
// 내부적으로 일어나는 일:
// 1. initialized 체크 (false)
// 2. 영속성 컨텍스트에 실제 엔티티 로딩 요청
// 3. DB 조회
// 4. 실제 엔티티 생성
// 5. target에 실제 엔티티 연결
// 6. initialized = true로 변경
실제 메모리에서 일어나는일을 체크해보면 다음과 같습니다.
Member proxy = em.getReference(Member.class, 1L);
//프록시 객체에 Memberclass 만큼 메모리를 할당하는 껍데기 객체 생성
[메모리 변화 1단계 - DB 조회]
1. 영속성 컨텍스트가 DB에서 데이터를 조회
2. 실제 Member 엔티티 객체 생성
[Heap 메모리]
realMember = { // 새로 생성된 실제 엔티티
id: 1,
name: "John",
team: ...
}
proxy = { // 기존 프록시 객체
target: null, // 아직 연결 전
initialized: false,
id: 1
}
String name = proxy.getName(); // 이 시점에 초기화 발생
[메모리 변화 2단계 - 타겟 연결]
proxy의 target 필드가 실제 엔티티를 참조하도록 변경
[Heap 메모리]
realMember = { // 실제 엔티티
id: 1,
name: "John",
team: ...
}
proxy = {
target: ----→ realMember, // 실제 엔티티 참조
initialized: true, // 초기화 완료 표시
id: 1
}
proxy.getName(); // 두 번째 호출
[Heap 메모리]
// 이미 초기화되어 있으므로(initialized = true)
// proxy 객체는 target을 통해 바로 실제 엔티티의 메서드 호출
proxy.getName() --→ proxy.target.getName() --→ "John" 반환
이런 과정을 proxy 초기화 과정이라 할 수 있습니다.
요약하자면,
- proxy 객체란, 식별자로 초기화된 껍데기 객체입니다.
- 해당 객체의 실제 정보가 필요한 순간에, db를 조회하며, 껍데기 객체가 실제 객체를 참조하게됩니다.
이런 일련의 과정을 통하여, team 과 같은 불필요한 정보를 필요한 순간에만 조회할 수 있게 됩니다.
@Entity
public class Member {
@Id
private Long id;
// 다대일 관계에서 지연 로딩 설정
@ManyToOne(fetch = FetchType.LAZY)
private Team team;
}
정리하면,
- EntityManager의 find vs getReference 메서드 차이를 이용하여, 실제 객체를 가져올지, 필요한 순간에 값들을 가져올 proxy객체를 만들지 결정
- 연관 필드들의 설정값인 LAZY vs EAGER 를 이용하여, 실제 사용시점에 조회할지, 엔티티 조회시 조회할지 결정
여기서 의문점이 있었습니다. 그럼 EAGER가 필요한 시점이 있을까요? 무조건 지연로딩이 좋아보이는데?
- 실제로 JPA의 권장사항은 지연로딩이고, 함께 조회가 필요한 순간에는 join등을 사용하는게 좋다고 합니다.