1. 프록시란?
JPA에서 연관된 엔티티를 지연 로딩하기 위해 사용되는 객체다.
데이터베이스 접근을 지연시켜 성능을 최적화한다.
필요한 시점까지 데이터 로딩을 미뤄서 메모리 사용량을 줄이고 불필요한 쿼리 실행을 방지한다.
⭐지연 로딩
JPA에서 실제 엔티티 데이터가 필요할 때까지 데이터베이스에서 해당 데이터를 로드하지 않는 방식.
이를 통해 불필요한 데이터베이스 접근을 줄여 성능을 최적화한다.
2. 프록시의 필요성
양방향 연관 관계인 두 엔티티가 있다.
Member와 Team이라고 하자.
✅Member와 Team 코드
@Entity
@Getter
@Setter
public class Member{
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
@Getter
@Setter
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
두 엔티티가 이런 관계일 때, Member 객체만 필요할 때도 Team 객체와 조인이 일어나면서 불필요한 쿼리가 실행될 수 있다.
✅main 메서드
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUsername("ururuwave");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
tx.commit();
}catch (Exception e){
tx.rollback();
}finally {
em.close();
}
}
}
member에 username을 입력하고 member정보만 출력하는 간단한 코드다.
이럴때 MEMBER 테이블만 조회하고 싶은데 실제 실행되는 쿼리는 아래와 같다.
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name,
m1_0.USERNAME
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_ID=m1_0.TEAM_ID
where
m1_0.MEMBER_ID=?
현재 main 메서드 코드로는 TEAM 테이블이 필요 없는데 불필요한 조인이 이뤄지는 걸 볼 수 있다.
불필요한 DB 접근을 줄이기 위해 프록시가 필요하다.
3. 프록시 기초
3-1) em.getReference()
em.find()는 데이터베이스를 통해 실제 엔티티 객체를 조회한다.
em.getReference() 참조를 가지고 온다. ➡️ DB 조회를 미루는 가짜(프록시) 엔티티 객체 조회
쿼리가 실행되는게 아니다.
3-2) 프록시 객체의 구조
Hibernate에서 프록시는 실제 클래스를 상속 받아서 만들어진다.
그래서 프록시 객체는 실제 클래스와 겉모양이 같다.
프록시 객체는 실제 객체 참조(target)을 보관한다.
프록시 객체는 초기화되기 전까지 실제 데이터를 가지고 있지 않는다.
⭐Hibernate
: JPA 인터페이스와 클래스를 구현하여 개발자가 JPA를 통해 DB작업을 수행할 수 있도록 도와주는 ORM 프레임워크다.
3-3) 프록시 객체 초기화
- 프록시 객체 초기화는 실제 데이터베이스에 접근하여 프록시 객체가 참조하는 실제 엔티티의 데이터를 로드하는 과정이다.
- 초기화를 하기 전까지는 프록시 객체는 실제 엔티티 데이터에 접근할 수 없다.
- 프록시 객체 초기화는 프록시 객체 메서드가 호출될 때 이뤄진다.
위 그림의 프록시 객체를 초기화하는 과정을 설명해보자면
1. getName()
클라이언트가 Member 엔티티의 getName() 메서드를 호출한다.
이때 실제 Member 객체가 아니라 Member를 대신하는 프록시 객체가 반환된다.
2. 초기화 요청
프록시 객체가 영속성 컨텍스트에 초기화 요청을 전달한다.
3. DB 조회
영속성 컨텍스트는 DB에 접근하여 실제 Member 엔티티 데이터를 조회한다.
4. 실제 Entity 생성
비어있던 프록시 객체가 실제 엔티티 인스턴스의 데이터를 로드하여 초기화된다.
🤔Q. 실제 Entity가 이때 생성된다는 건 무슨 말이지?
프록시는 실제 Entity를 상속받아 만들어진다 했으니 실제 Entity는 이미 존재했어야하는거 아닌가?
💡A. 프록시 객체가 실제 Entity의 데이터를 사용할 수 있게 됐다고 받아들이면 된다.
client가 메서드를 호출하면 전체 엔티티 데이터가 로드된다. (특정 필드에 접근하는 메서드를 호출 했다고 해당하는 데이터만 로드되는건 아님) 프록시 객체 자체가 실제 엔티티 데이터를 담는 것은 아니다.
대신, 프록시 객체는 실제 엔티티 인스턴스에 접근하여 그 데이터를 반환하는 방식으로 동작한다.
5. target.getName()
초기화가 완료된 후, 프록시 객체를 통해 실제 엔티티에 접근하여 getName() 메서드를 호출한다.
이제 클라이언트는 Member 엔티티의 실제 데이터를 얻을 수 있게 된다.
4. 프록시 특징
1️⃣프록시 객체는 처음 사용할 때 한 번만 초기화된다.
2️⃣프록시 객체를 초기화 할때, 프록시 객체가 실제 엔티티 데이터를 담는 다거나, 실제 엔티티로 바뀌는 것이 아니다.
프록시 객체를 통해 실제 엔티티에 접근이 가능한 것이다.
초기화 전과 후를 클래스 타입을 반환하여 확인해보자.
✅코드 예시
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUsername("ururuwave");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("before findMember = " + findMember.getClass());
System.out.println("findMember.username = " + findMember.getUsername());
System.out.println("after findMember = " + findMember.getClass());
tx.commit();
}catch (Exception e){
tx.rollback();
}finally {
em.close();
}
}
✅출력 확인
before findMember = class hellojpa.Member$HibernateProxy$0CAbyL8Q
findMember.username = ururuwave
after findMember = class hellojpa.Member$HibernateProxy$0CAbyL8Q
초기화 된 후에도 findMember는 여전히 프록시 객체다.
실제 엔티티로 바뀌는게 아니라는 것을 알 수 있다.
3️⃣프록시 객체는 원본 엔티티를 상속받기 때문에 타입 체크시 주의해야한다. ('==' 비교보다 'instance of'를 사용해야한다.)
'==' 는 두 객체의 메모리 주소를 비교한다.
'instanceof'는 객체가 특정 클래스의 인스턴스이거나 그 서브클래스의 인스턴스인지 확인한다.
프록시 객체가 실제 엔티티 클래스의 서브클래스로 생성되기 때문에 instanceof 연산자를 사용하면 프록시 객체와 실제 엔티티객체가 같다고 나온다.
직접 코드로 확인을 해보자.
💡포인트
- 초기화 될 때, 어떤 쿼리가 실행될까?
- 초기화 된 후, 정말 쿼리 실행없이 데이터를 조회할까?
- '=='와 'instanceof'로 타입 비교 결과 확인하기
✅타입 비교 테스트 예시
✔️Member 엔티티의 team 필드를 지연 로딩으로 설정한다.
@Entity
@Getter
@Setter
public class Member extends BaseEntity{
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.LAZY)//지연 로딩
@JoinColumn(name = "TEAM_ID")
private Team team;
}
✔️main 메서드
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("ururuwave");
member.setTeam(team); //Member 엔티티의 team 필드 설정(연관관계 설정)
em.persist(member);
//변경사항 DB에 반영하고 영속성컨텍스트 초기화
em.flush();
em.clear();
//프록시 객체 로드
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("===========Member 엔티티 초기화 전==========");
Team proxyTeam = findMember.getTeam(); //team은 프록시 객체로 로드됨
System.out.println("===========Member 엔티티 초기화 후==========");
System.out.println("proxyTeam = " + proxyTeam.getClass());
System.out.println("===========Team 엔티티 초기화 전==========");
Hibernate.initialize(proxyTeam);
System.out.println("===========Team 엔티티 초기화 후==========");
Member findMember2 = em.getReference(Member.class,member.getId());
System.out.println("==========쿼리 실행 유무 확인 START===========");
System.out.println("findMember2.getUsername() = " + findMember2.getUsername());
System.out.println("==========쿼리 실행 유무 확인 END===========");
// == 비교
if (proxyTeam.getClass() == Team.class) {
System.out.println("proxyTeam과 Team의 타입이 같다");
} else {
System.out.println("proxyTeam과 Team의 타입이 다르다"); //실행
}
// instanceof 비교
if (proxyTeam instanceof Team) {
System.out.println("proxyTeam이 Team의 인스턴스다."); //실행
} else {
System.out.println("proxyTeam이 Team의 인스턴스가 아니다.");
}
tx.commit();
}catch (Exception e){
tx.rollback();
}finally {
em.close();
}
}
✅콘솔 확인
===========Member 엔티티 초기화 전==========
Hibernate:
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
m1_0.TEAM_ID,
m1_0.USERNAME
from
Member m1_0
where
m1_0.MEMBER_ID=?
===========Member 엔티티 초기화 후==========
proxyTeam = class hellojpa.Team$HibernateProxy$OzaI4kkX
===========Team 엔티티 초기화 전==========
Hibernate:
select
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name
from
Team t1_0
where
t1_0.TEAM_ID=?
===========Team 엔티티 초기화 후==========
==========쿼리 실행 유무 확인 START===========
findMember2.getUsername() = ururuwave
==========쿼리 실행 유무 확인 END===========
proxyTeam과 Team의 타입이 다르다
proxyTeam이 Team의 인스턴스다.
💡포인트 체크
- 초기화 될 때, 어떤 쿼리가 실행될까?
- findMember.getTeam()을 호출하여 프록시 객체가 초기화된다.
Member엔티티에 있는 메서드를 호출했으니 MEMBER 테이블의 전체 필드를 조회하는 쿼리가 실행됐다.
getTeam을 했어도 Team 엔티티를 초기화 하지않는다.
Team엔티티가 초기화되지 않은채 Team 프록시 객체만 생성된 것이다.
- Hibernate.initialize(proxyTeam); 를 호출하여 데이터베이스에서 Team의 실제 데이터를 로드하도록 했다. (초기화)
TEAM 테이블의 전체 필드를 조회하는 쿼리가 실행됐다.
Team의 데이터를 확인하는 부분이 없어서 딱히 필요한 작업은 아니었지만, 초기화되는 것을 확인하려고 넣은 코드다.
이를 통해, Member 엔티티에 Team 필드가 있어도 지연로딩 설정을 해두니 Team과 조인하지 않는 쿼리가 실행됨을 알 수 있었다.
- 초기화 된 후, 정말 쿼리 실행없이 데이터를 조회할까?
초기화 된 후, findMember2.getUsername()으로 동일한 영속성 컨텍스트 내에서 이미 초기화된 프록시 객체에 접근했다.
쿼리실행없이 username을 잘 가져온 것을 확인할 수 있다.
- '=='와 'instanceof'로 타입 비교 결과 확인하기
Team 프록시 객체와 실제 Team 클래스의 타입을 비교한 결과 instanceof 연산자를 써야 타입 체크 시 문제가 없다는 것을 알 수 있다.
💡JPA를 사용할 때 타입 체크 시 항상 instanceof 연산자를 사용하는 것이 유지보수 측면에서 더 낫다.
왜냐하면, instanceof를 사용하면 프록시 객체와 실제 엔티티를 비교하든 실제 엔티티끼리 비교하든 일관되게 동작하기 때문이다. '=='비교와 함께 사용하는 것보다 항상 instanceof를 사용하는 것이 코드 일관성이 유지되어 좋기도 하고 매번 프록시 객체의 존재를 고려할 필요가 없기때문에 더 편하다.
4️⃣영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
같은 영속성컨텍스트에서 관리되는 엔티티들은 '=='비교를 했을 때, true를 반환해야한다. (같은 인스턴스임을 보장)
왜냐하면, 동일한 영속성 컨텍스트 내에 동일한 엔티티 인스턴스를 사용한다는 일관성을 유지하기 위함이다.
✅코드 예시(1)
이 예시는 Member 엔티티에 Team필드가 지연로딩 설정이 안되어있는 상태다.
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUsername("ururuwave");
em.persist(member);
//변경사항 DB에 반영하고 영속성컨텍스트 초기화
em.flush();
em.clear();
Member member1 = em.find(Member.class, member.getId());
System.out.println("member1 = " + member1.getClass());
Member reference = em.getReference(Member.class, member.getId());
System.out.println("reference = " + reference.getClass());
System.out.println("member1 == reference: "+(member1==reference));
tx.commit();
}catch (Exception e){
tx.rollback();
}finally {
em.close();
}
}
여기서 중요한 포인트는 member1과 reference가 같은 id값을 바라보고 있다는 것이다.
이 코드에는 member가 하나뿐이다.
✅콘솔 확인(1)
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name,
m1_0.USERNAME
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_ID=m1_0.TEAM_ID
where
m1_0.MEMBER_ID=?
member1 = class hellojpa.Member
reference = class hellojpa.Member
member1 == reference: true
Member member1 = em.find(Member.class, member.getId()); 이 코드가 실행되고 엔티티가 영속성 컨텍스트에서 관리되기 시작한다.
그래서 이 다음에 em.getReference를 호출해도 reference는 실제 엔티티를 반환한다.
✅코드 예시(2)
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
// Member 엔티티 생성 및 저장
Member member1 = new Member();
member1.setUsername("ururuwave");
em.persist(member1);
Member member2 = new Member();
member2.setUsername("anotherMember");
em.persist(member2);
// DB 반영 및 영속성 컨텍스트 초기화
em.flush();
em.clear();
// 엔티티 조회
Member findMember1 = em.find(Member.class, member1.getId());
Member findMember2 = em.getReference(Member.class, member2.getId());
//클래스 타입 확인
System.out.println("findMember1 = " + findMember1.getClass());
System.out.println("findMember2 = " + findMember2.getClass());
// 다른 인스턴스 비교
System.out.println("findMember1 == findMember2 : " + (findMember1 == findMember2)); // false
tx.commit();
}catch (Exception e){
tx.rollback();
}finally {
em.close();
}
}
멤버를 둘로 만들었다.
findMember1은 member1의 id에 해당하는 객체를 em.find()로 가져오고, findMember2는 member2의 id에 해당하는 객체를 em.getReference를 호출해서 가져온다.
✅콘솔 확인(2)
findMember1 = class hellojpa.Member
findMember2 = class hellojpa.Member$HibernateProxy$yyHYMU6B
findMember1 == findMember2 : false
(1)번 코드 예시와 달리 em.getReference가 잘 호출돼서 findMember2가 프록시 객체임을 알 수 있다.
findMember1과 findMember2가 같은 Member클래스를 참조하지만 동일한 인스턴스가 아니기때문이다.
5️⃣영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.
(실무에서 자주 마주하는 문제다.)
보통 트랜잭션의 시작과 끝과 함께 영속성 컨텍스트도 시작하고 끝낼 수 있게 맞추는데 트랜잭션이 끝나고 나서 프록시를 조회하려고하면 오류가 나는 거다.
✅코드 예시
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
// Member 엔티티 생성 및 저장
Member member = new Member();
member.setUsername("ururuwave");
em.persist(member);
// DB 반영 및 영속성 컨텍스트 초기화
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.getClass() = " + findMember.getClass());
//findMember를 영속성 컨텍스트에서 분리(준영속 상태)
em.detach(findMember);
findMember.getUsername();
tx.commit();
}catch (Exception e){
tx.rollback();
e.printStackTrace(); //예외 출력
}finally {
em.close();
}
}
em.detach(findMember)로 findMember를 영속성 컨텍스트에서 분리해서 준영속 상태로 만들었다.
그리고 findMember.getUsername()를 호출하여 프록시가 초기화되어야하는데 오류가 날 것이다.
(*참고로 em.close(), em.clear()를 해도 같은 오류가 발생한다.)
✅콘솔 확인
findMember.getClass() = class hellojpa.Member$HibernateProxy$IZi9YLbh
org.hibernate.LazyInitializationException: could not initialize proxy [hellojpa.Member#1] - no Session
findMember는 Member 프록시 객체다.
그런데 초기화 하기전에 준영속 상태로 만드니까 프록시를 초기화 할 수 없다고 하는org.hibernate.LazyInitializationException 예외가 발생한다.
5. 프록시 확인
✔️프록시 인스턴스 초기화 여부 확인
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
// Member 엔티티 생성 및 저장
Member member = new Member();
member.setUsername("ururuwave");
em.persist(member);
// DB 반영 및 영속성 컨텍스트 초기화
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("==초기화 전==");
System.out.println("findMember is Loaded = " + emf.getPersistenceUnitUtil().isLoaded(findMember));
Hibernate.initialize(findMember); //강제 초기화
System.out.println("==초기화 후==");
System.out.println("findMember is Loaded = " + emf.getPersistenceUnitUtil().isLoaded(findMember));
tx.commit();
}catch (Exception e){
tx.rollback();
e.printStackTrace(); //예외 출력
}finally {
em.close();
}
}
EntityManagerFactory인 emf를 쓴다.
getPersistenceUnitUtil().isLoaded(프록시 인스턴스)를 호출했을 때, 초기화가 됐다면 true, 초기화가 안됐다면 false를 반환한다.
✅콘솔 확인
==초기화 전==
findMember is Loaded = false
Hibernate:
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name,
m1_0.USERNAME
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_ID=m1_0.TEAM_ID
where
m1_0.MEMBER_ID=?
==초기화 후==
findMember is Loaded = true
초기화 전에는 false가 반환 됐고, 강제 초기화를 하니 쿼리가 실행됐다.
초기화 후에는 true가 반환 됐다.
'인프런 김영한 강의 정리 > 자바 ORM 표준 JPA 프로그래밍 기본편' 카테고리의 다른 글
JPA 기본 | 영속성 전이(CASCADE)와 고아 객체 (0) | 2024.07.13 |
---|---|
JPA 기본 | 즉시 로딩과 지연 로딩 (0) | 2024.07.13 |
JPA 기본 | @MappedSuperclass (0) | 2024.07.10 |
JPA 기본 | 상속관계 매핑 (0) | 2024.07.10 |
JPA 기본 | 다양한 연관관계 매핑 (0) | 2024.07.05 |