1. Hibernate 프록시 객체
JPA의 영속성 컨텍스트를 통해 User 정보를 조회할 때, 연관된 Team 정보도 함께 가져올 수 있습니다. 그러나 Team 정보가 당장 필요하지 않은 경우, 이를 지연 로딩(Lazy Loading) 방식으로 처리할 수 있습니다. 이때 Hibernate는 Team 객체 대신 프록시 객체를 반환하여 실제 데이터 조회를 필요 시점으로 지연시킵니다.
getReference()를 통해 프록시 객체를 생성할 수 있습니다. 이 프록시 객체는 실제 엔터티를 상속하며, 필요한 시점에 실제 데이터를 조회합니다.
User findUser = em.getReference(User.class, user.getId());
System.out.println("class = " + findUser.getClass());
- 출력 결과:
class = 생략.User$HibernateProxy$1bxcIiNX
위 출력에서 볼 수 있듯이, findUser는 User 클래스의 실제 객체가 아니라 Hibernate의 프록시 객체입니다.
프록시 객체의 부모 클래스는 실제 User 클래스입니다.
User findUser = em.getReference(User.class, user.getId());
System.out.println("proxy객체의 부모클래스" + findUser.getClass().getSuperclass());
- 출력 결과:
proxy객체의 부모클래스class jpashop.jpabook.jpashop.domain2.User
1.1. 초기화
프록시 객체는 처음에 빈 상태로 생성되며, 실제 데이터가 필요할 때 해당 데이터를 로드합니다. 이 과정을 초기화라고합니다.
초기화 시점은 다음과 같습니다.
1) getter로 필드명 불러올 때
findUser.getName();
findUser.getEmail();
findUser.getAddress();
프록시 객체가 초기화되는 가장 일반적인 시점은 getter 메서드를 호출할 때입니다. 예를 들어, getName() 메서드를 호출하면, 프록시 객체는 실제 데이터를 DB에서 조회하여 로드합니다.
하지만 getId()와 같은 메서드는 초기화되지 않습니다. 왜냐하면 ID는 이미 프록시 객체가 가진 정보이기 때문입니다.
2) Hibernate.initialize()
Hibernate.initialize(findUser);
getter 말고도 "Hibernate.initialize()"를 이용하여 명시적으로 프록시 객체를 초기화할 수 있습니다.
이때, 프록시 객체는 초기화된 후에도 도메인 객체로 전환되지 않고, 계속해서 프록시 객체로 유지됩니다. 즉, 프록시 객체는 데이터베이스에서 실제 데이터를 가져온다고 해도, 본래의 프록시 상태를 유지한 채로 존재합니다.
정리하면
- 초기화된 프록시 객체는 도메인 객체로 변환되지 않고, 계속해서 프록시 객체로 남습니다. 이는 동등성을 위해서입니다.
- 데이터베이스에서 실제 데이터를 로드한 후에도 프록시 객체는 여전히 프록시로 존재하며, 도메인 객체를 "가리키게" 됩니다.
2. 즉시/ 지연로딩
2.1. 즉시 로딩
즉시 로딩은 연관된 객체를 한꺼번에 불러오는 기능입니다. FetchType.EAGER로 설정합니다.
@Entity
public class User {
@Id @GeneratedValue
@Column(name = "user_id")
private Long id;
@Column(name = "user_name")
private String username;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
다음의 코드 실행 시
Team team = new Team();
team.setTeamName("team");
em.persist(team);
User user = new User();
user.setUsername("user1");
user.setTeam(team);
em.persist(user);
User findUser = em.find(User.class, user.getId());
즉시 로딩은 연관 데이터를 가져오기 위해 INNER JOIN 또는 OUTER JOIN을 사용합니다.
2.2. 지연 로딩
지연 로딩은 연관된 엔티티를 프록시 객체로 초기화하고, 실제 데이터는 필요한 시점에 데이터베이스에서 조회하는 방식입니다. 즉시 로딩과 달리, 지연 로딩은 연관된 데이터를 처음부터 함께 조회하지 않고 필요한 순간까지 지연시킵니다.
FetchType.LAZY로 설정합니다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
프록시 객체를 설명했을 때랑 마찬가지로, 실제 데이터가 필요한 시점에 쿼리를 실행하여 데이터를 조회합니다. 이렇게 데이터를 나중에 로딩하는 방식은 초기 로딩 성능을 높이고 불필요한 데이터 조회를 방지할 수 있습니다.
- @ManyToOne, @OneToOne은 기본적으로 즉시 로딩.
- @OneToMany, @ManyToMany는 기본적으로 지연 로딩.
ORM 사용 시 주의점: 즉시 로딩(Eager Loading)의 문제점
ORM(Object-Relational Mapping)을 사용할 때, 즉시 로딩(Eager Loading)은 연관된 모든 엔티티를 한 번에 가져오는 편리함을 제공하지만, 잘못 사용하면 성능에 큰 문제를 초래할 수 있습니다.
@Table(name = "course")
public class Course {
@Id
private Long courseId;
private String title;
@ManyToOne()
private Professor professor;
}
예시로 Course객체는 Professor객체와 다대일 관계를 가진 상태이고 즉시 로딩으로 설정한 상태입니다.
- find 연산(객체지향 쿼리)
Course course = em.find(Course.class, 1L);
다음과 같이 객체 지향 쿼리를 사용했을 때
위에서 설명했듯이 즉시 모든 연관된 데이터를 join을 통해 가져옵니다.
이는 연관된 엔티티들이 한 번의 쿼리로 모두 조회되도록 하여 sql쿼리 횟수를 줄여주지만, 불필요한 연관 데이터까지 모두 가져올 수 있다는 것입니다. 특히, 여러 개의 조인이 포함될 경우, 실제로 필요하지 않은 데이터까지 함께 로드되기 때문에 쿼리 실행 시간이 길어지거나 성능 저하를 초래할 수 있습니다.
- JPQL
JPQL은 JPA에서 제공하는 SQL과 유사한 문법입니다.
Query query = em.createQuery("select c from Course c");
List<Course> courses = query.getResultList();
위의 코드를 실행하면
화면에는 다 안담겼지만 무수한 쿼리를 추가적으로 수행합니다.
JPQL에서 select c from Course c는 SQL로 변환하면 SELECT * FROM Course; 형태입니다.
만약 Course 엔티티에 EAGER 로딩이 설정된 연관 엔티티가 있다면, Course 엔티티를 조회한 후에 연결된 Professor 엔티티를 별도의 쿼리로 조회하게 됩니다.
예를 들어, 10개의 Course 엔티티가 조회되었다면, 각 Course에 대해 추가로 Professor를 조회하는 쿼리가 10번 실행될 수 있습니다. 이로 인해 총 11번의 쿼리가 실행되며, 이를 N+1 문제라고 합니다.
N+1 문제는 추가적인 쿼리가 발생하여 성능 저하를 초래할 수 있으며, 특히 대규모 데이터셋에서 성능 문제를 일으킬 수 있습니다. 이를 해결하기 위한 일반적인 방법은 "LAZY 로딩 전략" + "페치 조인(Fetch Join)"을 사용하는 것입니다.
LAZY 로딩을 통해 연관 엔티티가 즉시 로딩되지 않게 막습니다.
하지만 다음과 같이 프록시 객체를 초기화하는 시점에서는 결국 N+1처럼 추가쿼리가 생성됩니다.
for (Course course: courses) {
course.getProfessor().getProfessorId();
}
이를 방지하지 위해서 다음과 같이 join fetch로 join 해서 한꺼번에 가져오라고 명시적으로 적어야 합니다.
"select c from Course c join fetch c.professor"
페치 조인은 JPQL 쿼리 내에서 연관 엔티티를 함께 조회하도록 지시하여 추가 쿼리를 방지합니다. LAZY 로딩은 실제로 연관 엔티티가 필요할 때까지 로딩을 지연시킴으로써 불필요한 쿼리 실행을 줄입니다.
결론은 연관 엔티티의 로딩 전략을 지연 로딩(LAZY)으로 설정하고 필요할 때만 페치 조인 사용을 사용하는 것이 권장됩니다.
'컴퓨터 > JAVA' 카테고리의 다른 글
Spring - Log 레벨 이해 및 JAVA의 로그 프레임워크 종류 (0) | 2025.01.16 |
---|---|
JPA - Spring Boot의 @RestController, Hibernate Lazy Loading 직렬화 (0) | 2024.12.19 |
JPA - 연관관계 매핑 1(외래키 설정) (0) | 2024.11.28 |
JPA - 영속성 컨텍스트(Persistence Context) (0) | 2024.11.23 |
JAVA - 일급 컬렉션(First-Class Collection) (0) | 2024.10.31 |