즉시 로딩 (Eager Loading)
- 정의
- 연관된 엔티티를 함께 로딩하는 방식
- 기본 엔티티를 조회할 때 연관된 엔티티도 즉시 데이터베이스에서 조회
- 동작 원리
- 엔티티 조회 시 연관된 엔티티도 함께 쿼리를 실행
- JPQL 또는 SQL에서 JOIN을 사용하여 데이터를 한 번에 가져옴
- 장점
- 연관된 데이터를 미리 로딩하므로 추가적인 데이터베이스 호출을 방지
- Lazy Loading의 LazyInitializationException과 같은 문제를 방지
- 단점
- 연관된 엔티티를 항상 로딩하므로 불필요한 데이터가 로드되어 메모리와 성능에 영향을 줄 수 있음
- 복잡한 관계에서는 쿼리가 비대해지고 성능이 저하될 수 있음
- 사용 사례
- 연관된 엔티티를 자주 사용하거나 함께 필요한 경우
Ex) User와 UserProfile 관계에서 사용자 정보와 프로필 정보를 항상 함께 조회할 때
- 연관된 엔티티를 자주 사용하거나 함께 필요한 경우
사용 예시
- 사용자 엔티티(User Entity) 내에 주문 엔티티(Order Entity)로 @OneToMany 관계로 연결이 되어있을 경우
- 사용자 엔티티(User Entity)를 조회할 때 주문 엔티티(Order Entity)도 함께 조회
UserEntity
@Getter
@ToString
@Entity
@Table(name = "tb_user")
public class UserEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_sq")
private long userSq;
@Column(name = "user_id")
private String userId;
@Column(name = "user_pw")
private String userSt;
@OneToMany(fetch = FetchType.EAGER, mappedBy = "userInfo")
private List<OrderEntity> orders;
}
OrderEntity
@Entity
@Table(name = "tb_order")
@Getter
public class OrderEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_sq")
private long orderSq;
@Column(name = "order_nm")
private String orderNm;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_sq")
private UserEntity userInfo;
}
조회 결과
- UserEntity는 즉시 로딩(FetchType.EAGER)으로 지정하여서 UserEntity를 호출하는 메서드만 불러도 아래와 같이 출력
- 즉, @OneToMany인 UserEntity와 @ManyToOne인 OrderEntity의 조회 결과로는 LEFT JOIN이 되어서 UserEntity 값이 모두 출력
Hibernate :
select
userentity0_.user_sq as user_sq1_1_0_,
userentity0_.user_sq as user_id2_1_0_,
userentity0_.user_sq as user_pw3_1_0_,
orders1_.user_sq as user_sq3_0_1_,
orders1_.order_sq as user_sq1_0_1_,
orders1_.order_sq as user_sq1_0_2_,
orders1_.order_nm as user_nm2_0_2_,
orders1_.user_sq as user_sq3_0_2_
from
tb_user userentity0_
left outer join
tb_order orders1_
on userentity0_.user_sq=orders1_.user_sq
where
userentity0_.user_sq=?
지연 로딩 (Lazy Loading)
- 정의
- 연관된 엔티티를 실제로 필요할 때까지 로딩하지 않는 방식
- 기본 엔티티를 조회할 때는 프록시 객체만 생성되고, 연관된 데이터를 사용할 때 데이터베이스에서 추가로 조회
- 동작 원리
- 엔티티 조회 시 연관된 엔티티는 프록시 객체로 대체
- 연관된 데이터가 실제로 호출되면 데이터베이스에서 쿼리가 실행되어 데이터를 로드
- 프록시 객체가 실제 데이터를 대체
- 장점
- 초기 로딩 시 불필요한 데이터 조회를 방지하여 성능을 최적화
- 메모리 사용량 감소
→ 필요할 때만 데이터를 로딩하므로 메모리 사용량이 효율적
- 단점
- N+1 문제
→ 여러 연관 엔티티를 반복적으로 호출할 때 추가 쿼리가 발생 가능 - 영속성 컨텍스트가 닫히면 프록시 객체를 초기화할 수 없어 LazyInitializationException이 발생 가능
- N+1 문제
- 사용 사례
- 연관된 엔티티를 자주 사용하지 않거나 특정 상황에서만 필요할 때
Ex) Order와 OrderDetails 관계에서 주문 상세는 특정 조건에서만 조회될 때
- 연관된 엔티티를 자주 사용하지 않거나 특정 상황에서만 필요할 때
사용 예시
UserEntity
@Getter
@ToString
@Entity
@Table(name = "tb_user")
public class UserEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_sq")
private long userSq;
@Column(name = "user_id")
private String userId;
@Column(name = "user_pw")
private String userSt;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "userInfo")
private List<OrderEntity> orders;
}
OrderEntity
@Entity
@Table(name = "tb_order")
@Getter
@ToString
public class OrderEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_sq")
private long orderSq;
@Column(name = "order_nm")
private String orderNm;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_sq")
private UserEntity userInfo;
}
조회 결과
- UserEntity는 즉시 로딩(FetchType.LAZY)으로 지정하여서 UserEntity를 호출하는 메서드를 불렀을 때 OrderEntity의 값을 불러오지 않고 UserEntity에 대해서만 아래와 같이 출력
Hibernate :
select
userentity0_.user_sq as user_sq1_1_0,
userentity0_.user_sq as user_id2_1_0,
userentity0_.user_sq as user_pw3_1_0
from
tb_user userentity0_
where userentity0_.user_sq=?
즉시 로딩과 지연 로딩의 비교표
항목 | 즉시 로딩 (Eager Loading) | 지연 로딩 (Lazy Loading) |
로딩 시점 | 엔티티 조회 시 즉시 로딩 | 실제 데이터가 필요할 때 |
쿼리 발생 | JOIN을 통해 한 번에 로딩 | 필요 시 추가 쿼리 발생 |
장점 | 데이터 접근 시 추가 쿼리 없음 | 초기 로딩 성능 최적화 |
단점 | 불필요한 데이터 로딩 가능, 성능 저하 | N+1 문제, LazyInitializationException 위험 |
사용 사례 | 연관 데이터를 자주 사용하는 경우 | 연관된 데이터가 자주 필요하지 않는 경우 |
지연 로딩 (Lazy Loading) 원리 이해하기
프록시(Proxy) 패턴
- 프록시 패턴 처리과정
- Subject(인터페이스), RealSubject(구현체), Proxy(대행자)로 구성
- Subject는 RealSubject, Proxy에 대한 공통 인터페이스로 사용
- RealSubject의 일을 Proxy가 위임(Delegate) 받아서 처리를 수행
- Client → Subject(interface)
- Subject 인터페이스는 RealSubject와 Proxy가 공통으로 구현해야 하는 인터페이스로 구성
- 클라이언트는 이 인터페이스에 접근하여 DoAction() 메서드 호출
- RealSubject(implements) → Subject(interace)
- Subject 인터페이스의 구현체인 RealSubject 클래스는 비즈니스 로직이 포함
- 클라이언트가 호출한 DoAction() 메서드의 구현 부분이 이 클래스에 포함
- Proxy → Subject(interace)
- Proxy는 RealSubject의 대리자(delegate) 역할
- 클라이언트가 메서드를 호출하면 Proxy는 필요에 따라 이 요청을 RealSubject에게 전달하거나 결과를 반환하거나 그 밖의 다양한 작업을 수행
- 이를 통해 객체 생성 시점 제어, 접근 제어, 또는 부가적인 로직 추가 등의 작업을 수행 가능
- Client → Subject(interface)
- 결론적으로 Client는 Subject 인터페이스의 DoAction()이라는 메서드를 호출
- 해당 호출에서는 실제 객체를 이용하지 않기에(Lazy Loading) 이에 따르는 구현체인 RealSubject 클래스의 DoAction() 메서드가 수행되는 것이 아닌 Proxy가 이를 위임(Delgate) 받아서 DoAction()의 수행 처리
프록시 객체 (Proxy Object)
- 실제 객체를 대신하여 사용되는 객체를 의미
- 대상 엔티티 클래스의 가짜 객체로 초기화되지 않은 상태에서는 엔티티의 식별자(ID)만 포함
- 실제 객체와 같은 인터페이스를 가지고 있으므로 실제 객체와 동일하게 사용 가능
- 실제 객체의 생성 시점을 제어하는 데 사용되며 이는 지연 로딩(Lazy Loading)의 핵심 원리
- 실제 객체가 필요하기 전까지 실제 객체의 생성을 지연하며, 실제 객체가 필요한 시점에 실제 객체를 생성하고 초기화
→ 엔티티의 프록시 객체에 접근하려고 할 때 초기화가 일어나며 해당 시점에 데이터베이스 쿼리 발생 - 프록시 객체를 이용함으로써 실제 객체의 생성 시점을 제어하고 불필요한 데이터베이스 조회를 줄여 성능을 향상시킬 수 있음
지연 로딩에서 바라보는 프록시 패턴 / 객체
- Member 엔티티를 '지연로딩(Lazy Loading)'으로 지정
- 최초 로드 시 '실제 엔티티를 가져오는 것'이 아닌 '엔티티의 프록시(MemberProxy)'를 먼저 로드
- 지연로딩으로 지정하면 프록시가 자동으로 생성되며 '실제 객체가 필요하기 전'까지 '실제 객체의 생성을 지연하기' 위해 사용
- Proxy를 통해 실제 객체를 이용하는 방법
- Client → MemberProxy
- Member Entity를 지연로딩 형태로 구성하였기에 MemberProxy가 생성
- 실제 객체를 사용하기 위해 getName() 메서드 호출
- MeberProxy → 영속성 컨텍스트 → DB 조회
- getName() 메서드는 데이터베이스를 조회하는 처리가 존재
- 영속성 컨텍스트에 등록된 Entity가 아니기 때문에 DB를 조회하고 영속성 컨텍스트에 등록
- 영속성 컨텍스트 → Member(Entity)
- 지연되었던 객체 생성 처리가 수행되어 실제 Member 엔티티 생성
- MeberProxy → Member(Entity)
- MemberProxy에서 target으로 가리키고 있는 Member Entity의 getName() 메서드를 호출함으로써 지연된 객체의 메서드를 호출
- Client → MemberProxy
결론
- 실무에서는 지연 로딩을 기본으로 함
- 다만, 지연 로딩은 N+1 문제나 InitailizationException 에러가 발생할 수 있음
- 특히 N+1 문제 발생 원인과 해결 방법을 정확히 파악해야 함
참고 자료
'Language > Spring' 카테고리의 다른 글
[SQL Mapper] Mybatis 개념 및 구조 (2) | 2024.12.02 |
---|---|
[JPA] N+1 문제 (1) | 2024.11.18 |
[Spring] Controller와 Rest Controller 차이 (0) | 2024.11.17 |
[Spring] DispatcherServlet의 개념과 흐름 (0) | 2024.11.17 |
[공통처리] Filter, Interceptor, AOP 차이점 및 흐름과 역할 (0) | 2024.11.15 |