연관관계인 `Employee`와 `Company` 엔티티가 있을 때, Employee가 Company를 참조하고 있기 때문에 Employee 정보를 조회하려고 하면 Company 정보도 조회하게 될 것입니다. Employee에 대한 정보만 필요하고 연관된 엔티티인 Company에 대한 정보가 필요하지 않은 경우, 이는 불필요한 작업(성능 낭비) 일 것입니다. JPA에서는 이러한 경우를 해결하기 위해 `지연 로딩`이라는 방식을 사용합니다.
지연 로딩: Lazy Loading
`지연 로딩`을 구현하기 위해 `프록시`를 활용합니다.
Proxy
프록시(Proxy)란 객체 지향 프로그래밍에서 실제 객체에 대한 대리 객체를 생성하여 대신 사용하는 패턴을 말합니다. JPA에서는 프록시 객체를 생성하여, 프록시 객체를 조회하도록 하고 연관된 데이터가 실제로 사용될 때 데이터베이스를 조회하도록 하였습니다. 이를 통해 불필요한 데이터베이스 접근을 최소화하여 성능을 향상시킬 수 있습니다.
프록시 객체를 만드는 방법은 `EntityManager.getReference()` 메서드를 사용하면 됩니다.
Employee employee = em.find(Employee.class, "Employee1"); // 실제 엔티티 객체 리턴
Employee employee = em.getReference(Employee.class, "Employee1"); // 프록시 객체 리턴
Proxy 객체 사용 과정
- getReference 메서드 호출:
getReference 메서드는 엔티티의 클래스와 기본 키 값을 인자로 받아서 프록시 객체를 생성합니다.
이때, 프록시 객체는 엔티티 클래스의 서브 클래스로 생성되며, 프록시 객체의 실제 타입은 엔티티의 타입입니다. - 영속성 컨텍스트 조회:
생성된 프록시 객체의 엔티티 클래스와 기본 키 값을 사용하여 영속성 컨텍스트에서 엔티티를 찾습니다.
영속성 컨텍스트에 해당 엔티티가 이미 존재한다면 프록시 객체 대신 실제 엔티티 객체를 반환할 수 있습니다. - 영속성 컨텍스트 초기화:
만약 영속성 컨텍스트에 엔티티가 존재하지 않는다면, 데이터베이스에서 해당 엔티티를 조회하여 영속성 컨텍스트에 초기화합니다.
이때 영속 상태의 엔티티로 만들어지며, 영속성 컨텍스트는 엔티티를 관리하게 됩니다. - 프록시 객체 반환:
초기화된 엔티티를 프록시 객체로 감싸서 반환합니다. 이 프록시 객체는 실제 데이터를 사용하는 시점까지 데이터베이스 조회를 미룹니다.
따라서 프록시 객체를 반환하는 시점에는 실제 데이터가 아니라 초기화되지 않은 프록시 객체가 반환됩니다. - 프록시 객체 사용:
프록시 객체를 사용하여 엔티티의 필드 값을 가져오거나 메서드를 호출할 때, 해당 필드에 대한 데이터베이스 조회가 실행됩니다.
이때 실제 데이터가 필요한 시점에는 데이터베이스 조회가 수행되어 필요한 데이터가 가져와집니다.
Parent와 Child 엔티티 지연 로딩 예시
Parent를 조회할 때, Child 엔티티는 프록시 객체로 대체됩니다. 이 프록시 객체는 실제 데이터를 가지고 있지 않고, 데이터베이스 조회를 미룹니다. Parent 엔티티에서 Child 필드에 접근하거나 Child 관련 메서드를 호출하는 시점에, 프록시 객체는 영속성 컨텍스트를 초기화하고 데이터베이스 조회를 수행하여 필요한 Child 데이터를 가져옵니다.
이러한 방식으로 Parent 엔티티를 조회할 때 즉시 Child 데이터까지 로딩하지 않고, 실제로 사용하는 시점에서만 필요한 Child 데이터를 가져옴으로 성능을 최적화할 수 있습니다.
Parent@Entity public class Parent { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY) // FetchType.LAZY 설정 private List<Child> children = new ArrayList<>(); // ... } @Entity public class Child { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @ManyToOne @JoinColumn(name = "parent_id") private Parent parent; // ... }
위 코드는 Parent 엔티티와 Child 엔티티의 연관 관계를 일대다 관계로 설정하고, FetchType.LAZY로 작성하여 지연 로딩을 설정하였습니다.
아래 코드는 em.find() 메서드를 호출할 때 Parent 엔티티 객체가 생성되고, parent 객체의 children 필드에는 프록시 객체가 할당됩니다. 그 후 parent.getChildren()을 호출하면 실제로 데이터베이스 조회가 발생하여, 필요한 Child 데이터를 가져옵니다.Parent parent = em.find(Parent.class, 5L); // 프록시 객체 생성, 데이터베이스 조회는 실행되지 않음 List<Child> children = parent.getChildren(); // Child 필드 접근 시 데이터베이스 조회 발생
지연 로딩 사용
위 예제 코드를 보면 알겠지만, getReference 메서드를 사용하지 않았는데 어떻게 프록시 객체가 생성되었을까요??
프록시 객체가 생성되는 것은
- `getReference` 메서드를 사용하거나
- `fetch = FetchType.LAZY`로 설정된 필드에 접근할 때 입니다.
때문에 위 코드에서는 em.find() 메서드를 호출하여도 fetch = FetchType.LAZY로 설정된 필드가 프록시 객체로 생성됩니다.
또한 `fetch` 속성을 사용하지 않아도, 기본적으로 `ToMany` 연관관계는 지연 로딩(LAZY)이 적용됩니다. 반대로 `ToOne` 연관관계는 즉시 로딩(EAGER)이 적용됩니다.
- @ManyToOne 또는 @OneToOne : 기본적으로 즉시 로딩 (FetchType.EAGER)
- @OneToMany 또는 @ManyToMany : 기본적으로 지연 로딩 (FetchType.LAZY)
즉시 로딩이란?
엔티티를 조회할 때, 해당 엔티티와 관련된 모든 연관 엔티티들을 한 번에 데이터베이스에서 조회하여 가져오는 방식을 말합니다. 엔티티와 연관 엔티티 간의 조인 쿼리를 사용하여 한 번에 모든 데이터를 가져오므로 지연 로딩에 비해 데이터베이스 조회가 한 번에 이루어지며, 엔티티와 모든 연관 엔티티들이 초기화됩니다.
즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용하여 쿼리 한 번으로 조회하고자 하는 엔티티를 모두 조회하는 것이 좋습니다.
'Spring Data > JPA' 카테고리의 다른 글
JPA '값 타입'의 선언과 활용, @AttributeOverride와 @ElementCollection, @CollectionTable 역할 (1) | 2023.08.23 |
---|---|
JPA 영속성 전이(CASCADE)와 고아 객체 제거: 데이터 일관성과 관리 효율 높이는 전략(방법) (0) | 2023.08.23 |
JPA 상속 관계 매핑 방법과 공통 매핑하는 방법 (0) | 2023.08.21 |
JPA 연관관계 매핑: 객체 간의 관계를 표현하고 데이터베이스에 저장 (0) | 2023.08.18 |
JPA 생성 및 수정 날짜 자동 처리를 위한 공통 엔티티 만들기 @MappedSuperclass, @EnableJpaAuditing (0) | 2023.08.17 |