반응형
해당 글에서는 Spring Boot JPA 내에 테이블 간의 관계에서 사용되는 FetchType 중 즉시로딩, 지연 로딩에 대해 알아봅니다.
💡 [참고] JPA 관련해서 구성 내용에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
분류 | 링크 |
Spring Boot Data JPA -1: ORM, JPA, Hibernate, QueryDSL 이론 | https://adjh54.tistory.com/421 |
Spring Boot Data JPA -2: 초기 환경 구성 + JpaRepository 활용 방법 | https://adjh54.tistory.com/422 |
Spring Boot Data JPA -3: 상세 JpaRepository 활용 방법 | https://adjh54.tistory.com/481 |
Spring Boot Data JPA 엔티티 어노테이션 -1 : 테이블 컬럼 단위 | https://adjh54.tistory.com/466 |
Spring Boot Data JPA 엔티티 어노테이션 -2 : 엔티티(테이블)간의 관계 | https://adjh54.tistory.com/477 |
Spring Boot Data JPA FetchType 이해하기 : 즉시/지연로딩 | https://adjh54.tistory.com/476 |
Spring Boot Data JPA + JPQL 활용 방법 | https://adjh54.tistory.com/479 |
Spring Boot Data JPA + Criteria API 활용 방법 | https://adjh54.tistory.com/483 |
Spring Boot Data JPA + QueryDSL 활용 방법-1 : 정의 및 구성요소 | https://adjh54.tistory.com/484 |
Spring Boot Data JPA + QueryDSL 활용 방법-2 : 초기 환경설정 및 활용예시 | https://adjh54.tistory.com/485 |
1) FetchType
💡 FetchType
- JPA에서 엔티티를 조회할 때 연관된 ‘엔티티 조회 방법을 결정하는 전략’을 의미합니다.
- javax.persistence.FetchType 패키지 내에 열거형(Eunm) 형태로 제공이 됩니다.
- FetchType에는 FetchType.EAGER 타입으로 ‘즉시 로딩(Eager Loading)’ 전략과 FetchType.LAZY 타입으로 ‘지연 로딩(Lazy Loading)’ 전략이 있습니다.
1. FetchType 타입은 언제 사용하는가?
💡 FetchType 타입은 언제 사용하는가?
- A라는 엔티티와 B라는 엔티티가 존재한다는 가정하였을때, 테이블 간의 관계(Join)에서 A 엔티티를 조회하였을 경우에 B 엔티티를 ‘함께 조회’할 것인지 아니면 ‘필요에 따라 조회할 것’인지 결정하는 데 사용이 됩니다.
💡 해당 예시의 엔티티 간의 관계
- UserEntity라는 tb_user 테이블과 OrderEntity라는 tb_order 테이블 간의 1 : N 관계를 형성하고 있습니다. 즉, 1개의 사용자는 여러개의 주문을 할 수 있다는 형태로 설계가 되어 있습니다.
- 이러한 관계는 FetchType에서는 UserEntity와 OrderEntity 간의 관계는 User 엔티티 기준으로 OneToMany 관계를 가집니다. 해당 관계에서 fetch 속성을 기반으로 FetchType.LAZY 속성값을 지정하였습니다.
- 이 지연 로딩(Lazy Loading)의 관계에서는 UserEntity를 조회할 때 OrderEntity는 조회하지 않고 OrderEntity 데이터가 실제로 사용될때 해당 데이터를 조회합니다.
@Entity
@Table(name = "tb_user")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_sq")
private long userSq;
@Column(name = "user_nm")
private String userNm;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "userInfo")
private List<OrderEntity> orders;
}
@Entity
@Table(name = "tb_order")
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.EAGER)
@JoinColumn(name = "user_sq")
private UserEntity userInfo;
}
2. 테이블 간의 관계에서 사용하는 어노테이션
💡 테이블 간의 관계에서 사용하는 어노테이션
- A와 B라는 엔티티가 존재한다는 가정하에 두 엔티티 간의 관계를 정의하는 어노테이션입니다.
어노테이션 | DB 매핑 타입 | 설명 |
@OneToOne | 1:1 관계 매핑 | 한 엔티티의 필드가 다른 엔티티와 1:1 관계에 있음을 나타냅니다. |
@OneToMany | 1:N 관계 매핑 | 한 엔티티가 다른 엔티티들과 1:N 관계에 있음을 나타냅니다. |
@ManyToOne | N:1 관계 매핑 | 한 엔티티가 다른 엔티티와 N:1 관계에 있음을 나타냅니다. |
@ManyToMany | N:N 관계 매핑 | 한 엔티티가 다른 엔티티들과 N:N 관계에 있음을 나타냅니다. |
2) 즉시 로딩(Eager Loading) : FetchType.EAGER
💡 즉시 로딩(Eager Loading) : FetchType.EAGER
- 엔티티를 조회하는 방법을 정의하는 전략 중 하나로 테이블 간의 관계에서 ‘A라는 엔티티를 조회할 때 연관된 B라는 엔티티도 함께 조회하는 방식’을 의미합니다.
- 이러한 방식은 한번의 쿼리로 필요한 모든 데이터를 가져올 수 있으나 필요하지 않은 데이터까지 조회되어 성능 이슈가 발생할 수 있습니다.
- 또한 즉시 로딩의 경우 연관된 ‘엔티티를 실제로 사용될 때 조회하는 방식’이므로 엔티티를 조회할 때 연관된 엔티티도 함께 조회하여 메모리에 로드합니다.
1. 즉시 로딩(Eager Loading) 사용예시
💡 사용예시
- 사용자 엔티티(User Entity) 내에 주문 엔티티(Order Entity)로 @OneToMany 관계로 연결이 되어있을 경우, 사용자 엔티티(User Entity)를 조회하는 경우 주문 엔티티(Order Entity)도 함께 조회되는 방식을 의미합니다.
💡 사용자 엔티티
- @OneToMany(fetch = FetchType.EAGER) 를 통해서 해당 엔티티를 조회하는 경우 OrderEntity 정보를 가져올 수 있도록 구성하였습니다.
- 또한 mappedBy 속성을 통해 OrderEntity의 필드명을 참조하였습니다.
/**
* tb_user 테이블과 매핑 객체
*
* @author : jonghoon
* @fileName : UserEntity
* @since : 2/8/24
*/
@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;
}
💡 주문 엔티티
- @ManyToOne(fetch = FetchType.LAZY)를 통해서 해당 엔티티를 조회하는 경우 UserEntity 정보를 가져올 수 없도록 구성하였습니다.
- 또한 @JoinColumn(name = "user_sq")를 통해 UserEntity의 키와 대응하도록 구성하였습니다.
/**
* tb_order 테이블과 매핑 객체
*
* @author : lee
* @fileName : OrderEntity
* @since : 4/9/24
*/
@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;
}
2. 즉시 로딩(Eager Loading) 조회결과
💡 즉시 로딩(Eager Loading) 조회결과
- UserEntity는 즉시 로딩(FetchType.EAGER)으로 지정하여서 UserEntity를 호출하는 메서드만 불러도 아래와 같이 출력이 되었습니다.
- 즉, @OneToMany인 UserEntity와 @ManyToOne인 OrderEntity의 조회 결과로는 LEFT JOIN이 되어서 UserEntity 값이 모두 출력되는 조회결과를 얻었습니다.
3) 지연 로딩(Lazy Loading): FetchType.LAZY
💡 지연 로딩(Lazy Loading): FetchType.LAZY
- 엔티티를 조회하는 방법을 정의한 전략 중 하나로 테이블 간의 관계에서 ‘A라는 엔티티를 조회할 때 연관된 B라는 엔티티도 있지만 A라는 엔티티만 조회하는 방식’을 의미합니다.
- 이러한 방식은 한번에 모든 데이터를 가져오지 않아 불필요한 데이터를 가져오는 것을 방지하여 성능을 향상할 수 있습니다.
1. 지연 로딩(Lazy Loading) 사용예시
💡 사용예시
- 사용자 엔티티(User Entity)와 주문 엔티티(Order Entity)가 @OneToMany 관계로 연결되어 있을 경우, 사용자 엔티티를 조회할 때 주문 엔티티는 조회하지 않습니다. 주문 엔티티는 실제로 사용될 때 데이터베이스에서 조회됩니다.
💡 사용자 엔티티
- @OneToMany(fetch = FetchType.LAZY)를 통해서 해당 엔티티를 조회하는 경우 OrderEntity 정보를 가져올 수 없도록 구성하였습니다.
- 또한 mappedBy 속성을 통해 OrderEntity의 필드명을 참조하였습니다
/**
* tb_user 테이블과 매핑 객체
*
* @author : jonghoon
* @fileName : UserEntity
* @since : 2/8/24
*/
@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;
}
💡 주문 엔티티
- @ManyToOne(fetch = FetchType.LAZY)를 통해서 해당 엔티티를 조회하는 경우 UserEntity 정보를 가져올 수 없도록 구성하였습니다.
- 또한 @JoinColumn(name = "user_sq")를 통해 UserEntity의 키와 대응하도록 구성하였습니다.
/**
* tb_order 테이블과 매핑 객체
*
* @author : lee
* @fileName : OrderEntity
* @since : 4/9/24
*/
@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;
}
2. 지연 로딩(Lazy Loading) 조회결과
💡 지연 로딩(Lazy Loading) 조회결과
- UserEntity는 즉시 로딩(FetchType.LAZY)으로 지정하여서 UserEntity를 호출하는 메서드를 불렀을 때 OrderEntity의 값을 불러오지 않고 UserEntity에 대해서만 아래와 같이 출력되었습니다.
4) 지연 로딩(Lazy Loading) 원리 이해하기
1. 프록시 패턴(Proxy Pattern)
💡 프록시 패턴(Proxy)
- 실제 객체를 대신하여 대리 객체(Proxy Object)를 생성하여 사용하는 디자인 패턴을 의미합니다.
💡 프록시 패턴 처리과정
- 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()의 수행처리가 됩니다.
2. 프록시 객체(Proxy Object)
💡 프록시 객체(Proxy Object)
- 실제 객체를 대신하여 사용되는 객체를 의미합니다. 실제 객체와 같은 인터페이스를 가지고 있으므로, 실제 객체와 동일하게 사용될 수 있습니다.
- 실제 객체의 생성 시점을 제어하는 데 사용되며 이는 지연 로딩(Lazy Loading)의 핵심 원리입니다. 실제 객체가 필요하기 전까지 실제 객체의 생성을 지연하며, 실제 객체가 필요한 시점에 실제 객체를 생성하고 초기화합니다.
- JPA에서는 엔티티를 프록시로 로딩하는 기능을 제공합니다. @ManyToOne, @OneToOne 등의 관계에서는 FetchType을 LAZY로 설정하면, 연관된 엔티티를 프록시 객체로 로딩하고, 실제 엔티티가 필요한 시점에 데이터베이스에서 조회합니다.
- 프록시 객체를 이용함으로써, 실제 객체의 생성 시점을 제어하고, 불필요한 데이터베이스 조회를 줄여 성능을 향상할 수 있습니다.
3. 지연 로딩에서 바라보는 프록시 패턴/객체
💡 지연 로딩에서 바라보는 프록시 패턴/객체
- 클라이언트는 Member라는 엔티티를 ‘지연로딩(Lazy Loading)’으로 지정하였습니다.
- Member라는 엔티티를 지연로딩으로 지정을 하게 되면 최초 로드 시 ‘실제 엔티티를 가져오는 것’이 아닌 ‘엔티티의 프락시(Proxy)’를 먼저 로드를 하게 됩니다.
- 지연로딩으로 지정하면 프록시가 자동으로 생기며, ‘실제 객체가 필요하기 전’까지 ‘실제 객체의 생성을 지연하기’ 위해 사용됩니다.
💡 Proxy를 통해 실제 객체를 이용하는 방법
1. Client → MemberProxy
- 클라이언트는 Member Entity를 지연로딩 형태로 구성하였기에 MemberProxy가 생성이 되었고, 클라이언트는 실제 객체를 사용하기 위해 getName()이라는 메서드를 호출합니다.
2, 3. MeberProxy → 영속성 컨텍스트 → DB 조회
- getName()이라는 메서드는 데이터베이스를 조회하는 처리가 있기에 영속성 컨텍스트를 통해 DB에 접근하여 DB데이터를 조회합니다.
4. 영속성 컨텍스트 → Member(Entity)
- 지연되었던 객체 생성 처리가 수행되어 실제 Entity가 생성됩니다.
5. MeberProxy → Member(Entity)
- MemberProxy에서 target으로 가리키고 있는 Member Entity의 getName() 메서드를 호출함으로써 지연된 객체의 메서드를 호출합니다.
오늘도 감사합니다. 😀
반응형