연관관계를 맺은 엔티티를 모두 포함시켜 조회하는 경우 비효율적일 수 있습니다. 굳이 불필요한 정보까지 조회하기 위해 쿼리가 복잡해지면 성능에도 영향을 끼칠 수 있기 때문입니다.
JPA(Java Persistence API)는 이러한 문제를 대비하여 지연 로딩과 프록시를 제공합니다. 지연 로딩은 연관된 엔티티를 실제로 필요할 때까지 로딩을 지연시키는 방식으로, 성능을 향상하고 불필요한 쿼리를 방지합니다. 프록시는 실제 엔티티 대신 사용되며, 필요한 경우 실제 엔티티를 가져오게 됩니다.
프록시(Proxy)
지연 로딩을 이해하려면, 먼저 프록시의 개념을 이해해야 합니다. JPA는 EntityManager를 통해 엔티티를 조회하는 메서드를 제공합니다. 앞으로 EntityManager를 em으로 축약하여 부르겠습니다.
메서드 | 설명 |
em.find() | 데이터베이스를 통해 실제 엔티티 객체를 조회한다. |
em.getReference() | 데이터베이스의 조회를 지연시켜 프록시(가짜) 엔티티 객체를 조회한다. |
예를 들어 Member 엔티티가 있으며, 해당 엔티티를 데이터베이스에 저장한 상태라고 가정하겠습니다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "name")
private String username;
}
em.find()
Member 엔티티를 em.find()로 조회한다면 다음과 같은 쿼리가 발생하게 됩니다.
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
Hibernate:
select
member0_.id as id1_4_0_,
member0_.name as name9_4_0_
from
Member member0_
where
member0_.id=?
findMember.id = 1
findMember.username = creator
em.getReference()
Member 엔티티를 em.getReference()로 조회하면 쿼리가 발생하지 않습니다.
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("findMember.id = " + findMember.getId());
findMember = class hello.jpa.Member$HibernateProxy$yJgMgbkR
findMember.id = 1
findMember의 클래스를 보면 Member 객체가 아닌 알 수 없는 객체로 생성되어 있음을 확인할 수 있습니다.
이후 Member 엔티티의 name(username) 컬럼을 가져오게 될 때 쿼리가 발생하게 됩니다.
System.out.println("findMember.username = " + findMember.getUsername());
Hibernate:
select
member0_.id as id1_4_0_,
member0_.name as name9_4_0_
from
Member member0_
where
member0_.id=?
findMember.username = creator
이러한 원인은 Member 객체가 아닌 Hibernate가 만든 프록시 객체로 생성되어 있기 때문입니다. findMember.getId() 메서드를 실행한 시점에는 프록시 객체가 id 값을 가지고 있어 쿼리가 발생되지 않습니다.
그러나 findMember.getUsername()를 실행하면 실제 엔티티 정보를 가져오기 위해 쿼리가 발생하게 됩니다.
프록시 구조

프록시는 실제 클래스를 상속 받아 생성됩니다. 실제 클래스와 겉모양이 동일하며, 사용자 입장에서 진짜 객체인지 프록시 객체인지 구분할 필요 없이 사용하면 됩니다.

프록시 객체는 실제 객체의 참조(target)을 보관하고 있으며, 프록시 객체를 호출하게 되면 프록시 객체는 실제 객체를 참조(target)을 통해 호출하게 됩니다.
em.getRefence() 메서드를 통해 엔티티를 가져오는 과정은 다음과 같습니다.

Member member = em.getReference(Member.class, member.getId());
member.getName();
- em.getReference()를 통해 프록시 객체를 가져와, getName()을 호출합니다.
- 프록시 객체의 target 값을 통해 영속성 컨텍스트에 초기화 요청을 합니다.
- 영속성 컨텍스트는 데이터베이스에 접근하여 조회 후, 실제 엔티티를 초기화합니다.
- 프록시 객체는 target을 통해 실제 엔티티로 접근하여 정보를 가져옵니다.
- 프록시 객체에 target 값이 할당된 시점부터 해당 프록시 객체는 초기화 동작이 일어나지 않습니다.
프록시 객체는 처음 사용될 때 한번 초기화를 합니다. 또한 프록시 객체를 초기화한다고 해서 프록시 객체가 실제 엔티티로 바뀌는 것이 아닙니다. 프록시 객체는 target을 통해 실제 엔티티를 이어주는 역할을 하는 것으로 이해하면 됩니다.
프록시 객체는 실제 엔티티를 상속 받기 때문에 프록시 객체와 원본 객체의 타입이 다르기 때문에 주의해야 합니다. 만약 불가피하게 비교를 하게 될 경우 '=='이 아닌 'instanceOf'를 사용하면 됩니다.
영속성 컨텍스트에 이미 엔티티가 저장되어 있다면 em.getReference()를 호출하여도 프록시 객체가 아닌 실제 엔티티를 반환합니다. JPA는 하나의 영속성 컨텍스트에 같은 엔티티의 동일성을 보장해 줍니다.
🤔 실무에서 만날 수 있는 문제?
프록시 객체를 통해 실제 엔티티를 참조할 수 있는 이유는 해당 객체가 영속 상태이기 때문입니다. 이러한 이유로 준영속 상태일 때 초기화 문제가 발생할 수 있습니다.
트랜잭션 범위 밖에 프록시 객체를 조회할 경우 Hibernate는 LazyInitializationException 예외를 발생시킵니다. 이를 방지하기 위해 OSIV(open-session-view) 설정을 통해 영속성 컨텍스트 생존 범위를 늘려 문제를 해결할 수 있으나, 영속성 컨텍스트 생존 범위가 늘어질수록 자원 낭비가 심해지기 때문에 실무에서 사용되지 않습니다.
자바 ORM 표준 JPA 프로그래밍 - 기본편 | 김영한 - 인프런
김영한 | JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 실무에서도
www.inflearn.com
'Spring > JPA' 카테고리의 다른 글
영속성 전이(CASCADE)와 고아 객체(ORPHAN) (0) | 2024.04.28 |
---|---|
즉시 로딩(Eager)과 지연 로딩(Lazy) (0) | 2024.04.28 |
상속관계 매핑 (0) | 2024.04.24 |
연관관계 매핑 정리 (0) | 2024.04.23 |
엔티티 매핑(Entity Mapping) - 기본 키 (1) | 2024.04.22 |
연관관계를 맺은 엔티티를 모두 포함시켜 조회하는 경우 비효율적일 수 있습니다. 굳이 불필요한 정보까지 조회하기 위해 쿼리가 복잡해지면 성능에도 영향을 끼칠 수 있기 때문입니다.
JPA(Java Persistence API)는 이러한 문제를 대비하여 지연 로딩과 프록시를 제공합니다. 지연 로딩은 연관된 엔티티를 실제로 필요할 때까지 로딩을 지연시키는 방식으로, 성능을 향상하고 불필요한 쿼리를 방지합니다. 프록시는 실제 엔티티 대신 사용되며, 필요한 경우 실제 엔티티를 가져오게 됩니다.
프록시(Proxy)
지연 로딩을 이해하려면, 먼저 프록시의 개념을 이해해야 합니다. JPA는 EntityManager를 통해 엔티티를 조회하는 메서드를 제공합니다. 앞으로 EntityManager를 em으로 축약하여 부르겠습니다.
메서드 | 설명 |
em.find() | 데이터베이스를 통해 실제 엔티티 객체를 조회한다. |
em.getReference() | 데이터베이스의 조회를 지연시켜 프록시(가짜) 엔티티 객체를 조회한다. |
예를 들어 Member 엔티티가 있으며, 해당 엔티티를 데이터베이스에 저장한 상태라고 가정하겠습니다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "name")
private String username;
}
em.find()
Member 엔티티를 em.find()로 조회한다면 다음과 같은 쿼리가 발생하게 됩니다.
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
Hibernate:
select
member0_.id as id1_4_0_,
member0_.name as name9_4_0_
from
Member member0_
where
member0_.id=?
findMember.id = 1
findMember.username = creator
em.getReference()
Member 엔티티를 em.getReference()로 조회하면 쿼리가 발생하지 않습니다.
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("findMember.id = " + findMember.getId());
findMember = class hello.jpa.Member$HibernateProxy$yJgMgbkR
findMember.id = 1
findMember의 클래스를 보면 Member 객체가 아닌 알 수 없는 객체로 생성되어 있음을 확인할 수 있습니다.
이후 Member 엔티티의 name(username) 컬럼을 가져오게 될 때 쿼리가 발생하게 됩니다.
System.out.println("findMember.username = " + findMember.getUsername());
Hibernate:
select
member0_.id as id1_4_0_,
member0_.name as name9_4_0_
from
Member member0_
where
member0_.id=?
findMember.username = creator
이러한 원인은 Member 객체가 아닌 Hibernate가 만든 프록시 객체로 생성되어 있기 때문입니다. findMember.getId() 메서드를 실행한 시점에는 프록시 객체가 id 값을 가지고 있어 쿼리가 발생되지 않습니다.
그러나 findMember.getUsername()를 실행하면 실제 엔티티 정보를 가져오기 위해 쿼리가 발생하게 됩니다.
프록시 구조

프록시는 실제 클래스를 상속 받아 생성됩니다. 실제 클래스와 겉모양이 동일하며, 사용자 입장에서 진짜 객체인지 프록시 객체인지 구분할 필요 없이 사용하면 됩니다.

프록시 객체는 실제 객체의 참조(target)을 보관하고 있으며, 프록시 객체를 호출하게 되면 프록시 객체는 실제 객체를 참조(target)을 통해 호출하게 됩니다.
em.getRefence() 메서드를 통해 엔티티를 가져오는 과정은 다음과 같습니다.

Member member = em.getReference(Member.class, member.getId());
member.getName();
- em.getReference()를 통해 프록시 객체를 가져와, getName()을 호출합니다.
- 프록시 객체의 target 값을 통해 영속성 컨텍스트에 초기화 요청을 합니다.
- 영속성 컨텍스트는 데이터베이스에 접근하여 조회 후, 실제 엔티티를 초기화합니다.
- 프록시 객체는 target을 통해 실제 엔티티로 접근하여 정보를 가져옵니다.
- 프록시 객체에 target 값이 할당된 시점부터 해당 프록시 객체는 초기화 동작이 일어나지 않습니다.
프록시 객체는 처음 사용될 때 한번 초기화를 합니다. 또한 프록시 객체를 초기화한다고 해서 프록시 객체가 실제 엔티티로 바뀌는 것이 아닙니다. 프록시 객체는 target을 통해 실제 엔티티를 이어주는 역할을 하는 것으로 이해하면 됩니다.
프록시 객체는 실제 엔티티를 상속 받기 때문에 프록시 객체와 원본 객체의 타입이 다르기 때문에 주의해야 합니다. 만약 불가피하게 비교를 하게 될 경우 '=='이 아닌 'instanceOf'를 사용하면 됩니다.
영속성 컨텍스트에 이미 엔티티가 저장되어 있다면 em.getReference()를 호출하여도 프록시 객체가 아닌 실제 엔티티를 반환합니다. JPA는 하나의 영속성 컨텍스트에 같은 엔티티의 동일성을 보장해 줍니다.
🤔 실무에서 만날 수 있는 문제?
프록시 객체를 통해 실제 엔티티를 참조할 수 있는 이유는 해당 객체가 영속 상태이기 때문입니다. 이러한 이유로 준영속 상태일 때 초기화 문제가 발생할 수 있습니다.
트랜잭션 범위 밖에 프록시 객체를 조회할 경우 Hibernate는 LazyInitializationException 예외를 발생시킵니다. 이를 방지하기 위해 OSIV(open-session-view) 설정을 통해 영속성 컨텍스트 생존 범위를 늘려 문제를 해결할 수 있으나, 영속성 컨텍스트 생존 범위가 늘어질수록 자원 낭비가 심해지기 때문에 실무에서 사용되지 않습니다.
자바 ORM 표준 JPA 프로그래밍 - 기본편 | 김영한 - 인프런
김영한 | JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 실무에서도
www.inflearn.com
'Spring > JPA' 카테고리의 다른 글
영속성 전이(CASCADE)와 고아 객체(ORPHAN) (0) | 2024.04.28 |
---|---|
즉시 로딩(Eager)과 지연 로딩(Lazy) (0) | 2024.04.28 |
상속관계 매핑 (0) | 2024.04.24 |
연관관계 매핑 정리 (0) | 2024.04.23 |
엔티티 매핑(Entity Mapping) - 기본 키 (1) | 2024.04.22 |