본문 바로가기

Language/Spring

[JPA] 엔티티 관계의 로딩 전략 - 즉시 로딩 / 지연 로딩

즉시 로딩 (Eager Loading)
  • 정의
    • 연관된 엔티티를 함께 로딩하는 방식
    • 기본 엔티티를 조회할 때 연관된 엔티티도 즉시 데이터베이스에서 조회
  • 동작 원리
    1. 엔티티 조회 시 연관된 엔티티도 함께 쿼리를 실행
    2. 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)
  • 정의
    • 연관된 엔티티를 실제로 필요할 때까지 로딩하지 않는 방식
    • 기본 엔티티를 조회할 때 프록시 객체만 생성되고, 연관된 데이터를 사용할 때 데이터베이스에서 추가로 조회
  • 동작 원리
    1. 엔티티 조회 시 연관된 엔티티는 프록시 객체로 대체
    2. 연관된 데이터가 실제로 호출되면 데이터베이스에서 쿼리가 실행되어 데이터를 로드
    3. 프록시 객체가 실제 데이터를 대체
  • 장점
    • 초기 로딩 시 불필요한 데이터 조회를 방지하여 성능을 최적화
    • 메모리 사용량 감소
      필요할 때만 데이터를 로딩하므로 메모리 사용량이 효율적
  • 단점
    • N+1 문제
      여러 연관 엔티티를 반복적으로 호출할 때 추가 쿼리가 발생 가능
    • 영속성 컨텍스트가 닫히면 프록시 객체를 초기화할 수 없어 LazyInitializationException이 발생 가능
  • 사용 사례
    • 연관된 엔티티자주 사용하지 않거나 특정 상황에서만 필요할 때
      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) 받아서 처리를 수행
      1. Client → Subject(interface)
        • Subject 인터페이스RealSubject와 Proxy가 공통으로 구현해야 하는 인터페이스로 구성
        • 클라이언트는 이 인터페이스에 접근하여 DoAction() 메서드 호출
      2. RealSubject(implements) → Subject(interace)
        • Subject 인터페이스의 구현체인 RealSubject 클래스비즈니스 로직이 포함
        • 클라이언트가 호출한 DoAction() 메서드의 구현 부분이 이 클래스에 포함
      3. Proxy → Subject(interace)
        • Proxy는 RealSubject의 대리자(delegate) 역할
        • 클라이언트가 메서드를 호출하면 Proxy는 필요에 따라 이 요청을 RealSubject에게 전달하거나 결과를 반환하거나 그 밖의 다양한 작업을 수행
        • 이를 통해 객체 생성 시점 제어, 접근 제어, 또는 부가적인 로직 추가 등의 작업을 수행 가능
    • 결론적으로 Client는 Subject 인터페이스의 DoAction()이라는 메서드를 호출
    • 해당 호출에서는 실제 객체를 이용하지 않기에(Lazy Loading) 이에 따르는 구현체인 RealSubject 클래스의 DoAction() 메서드가 수행되는 것이 아닌 Proxy가 이를 위임(Delgate) 받아서 DoAction()의 수행 처리
프록시 객체 (Proxy Object)
  • 실제 객체를 대신하여 사용되는 객체를 의미
  • 대상 엔티티 클래스의 가짜 객체초기화되지 않은 상태에서는 엔티티의 식별자(ID)만 포함
  • 실제 객체와 같은 인터페이스를 가지고 있으므로 실제 객체와 동일하게 사용 가능
  • 실제 객체의 생성 시점을 제어하는 데 사용되며 이는 지연 로딩(Lazy Loading)의 핵심 원리
  • 실제 객체가 필요하기 전까지 실제 객체의 생성을 지연하며, 실제 객체가 필요한 시점에 실제 객체를 생성하고 초기화
    엔티티의 프록시 객체에 접근하려고 할 때 초기화가 일어나며 해당 시점에 데이터베이스 쿼리 발생
  • 프록시 객체를 이용함으로써 실제 객체의 생성 시점을 제어하고 불필요한 데이터베이스 조회를 줄여 성능을 향상시킬 수 있음
지연 로딩에서 바라보는 프록시 패턴 / 객체

  • Member 엔티티를 '지연로딩(Lazy Loading)'으로 지정
  • 최초 로드 시 '실제 엔티티를 가져오는 것'이 아닌 '엔티티의 프록시(MemberProxy)'를 먼저 로드
  • 지연로딩으로 지정하면 프록시가 자동으로 생성되며 '실제 객체가 필요하기 전'까지 '실제 객체의 생성을 지연하기' 위해 사용
  • Proxy를 통해 실제 객체를 이용하는 방법
    1. Client → MemberProxy
      • Member Entity를 지연로딩 형태로 구성하였기에 MemberProxy가 생성
      • 실제 객체를 사용하기 위해 getName() 메서드 호출
    2. MeberProxy → 영속성 컨텍스트 → DB 조회
      • getName() 메서드는 데이터베이스를 조회하는 처리가 존재
      • 영속성 컨텍스트에 등록된 Entity가 아니기 때문에 DB를 조회하고 영속성 컨텍스트에 등록
    3. 영속성 컨텍스트 → Member(Entity)
      • 지연되었던 객체 생성 처리가 수행되어 실제 Member 엔티티 생성
    4. MeberProxy → Member(Entity)
      • MemberProxy에서 target으로 가리키고 있는 Member Entity의 getName() 메서드를 호출함으로써 지연된 객체의 메서드를 호출

결론
  • 실무에서는 지연 로딩을 기본으로 함
  • 다만, 지연 로딩은 N+1 문제나 InitailizationException 에러가 발생할 수 있음
  • 특히 N+1 문제 발생 원인과 해결 방법을 정확히 파악해야 함

참고 자료

https://adjh54.tistory.com/476#1.%20FetchType%20%ED%83%80%EC%9E%85%EC%9D%80%20%EC%96%B8%EC%A0%9C%20%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80%3F-1

 

[Java/JPA] Spring Boot Data JPA FetchType 이해하기: 즉시/지연 로딩(Eager/Lazy Loading)

해당 글에서는 Spring Boot JPA 내에 테이블 간의 관계에서 사용되는 FetchType 중 즉시로딩, 지연 로딩에 대해 알아봅니다.   💡 [참고] JPA 관련해서 구성 내용에 대해 궁금하시면 아래의 글을 참고

adjh54.tistory.com