@Entity
public class Member {
private String name;
@ManyToOne
private Group group;
}
위와 같은 엔티티에서 Member는 Group 엔티티와 연관관계를 맺고 있습니다. 만약 이 상태에서 이름(name)을 조회하는 경우에 Member 엔티티를 조회하는 과정에서 불필요한 Group 엔티티도 함께 조회하게 됩니다. 이런 경우에 name만을 얻고 싶었는데 Group 엔티티까지 조회하는 과정을 거치게 되는 불필요한 과정이 발생합니다.
JPA는 위와 같은 상황에서 실제로 사용되지 않는 엔티티의 조회는 미뤄두고 해당 엔티티가 실제로 사용되는 시점에 조회를 하는 지연 로딩 Lazy Loading을 제공하고 있습니다.
지연 로딩을 위해 조회를 지연시킬 수 있는 가짜 객체를 하나 두어야하는데 이를 프록시 객체라고 부릅니다.
프록시 객체는 다음과 같이 동작합니다.
지연 로딩 설정 시 실제 엔티티 대신 프록시 객체를 등록한다.
프록시 객체는 원본 클래스를 상속받아서 만들어지기 때문에 겉으로는 원본 클래스와 동일합니다. 따라서 사용자는 이 객체가 프록시인지 원본인이 신경쓰지 않고 사용할 수 있습니다.
또한 프록시 객체는 원본 객체에 대한 참조를 보관하고 있습니다.
프록시 객체는 첫 사용 시에 딱 한 번 초기화된다.
영속성 컨텍스트에 실제 엔티티가 생성되지 않았으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이 과정을
초기화라고 합니다.
초기화 과정에서 영속성 컨텍스트가 실제 엔티티 객체를 생성한다.
프록시 객체는 생성된 실제 엔티티 객체의 참조를 target 멤버 변수에 보관한다.
프록시 객체의 메소드를 호출하면 실제 엔티티의 메소드를 호출해준다.
JPA는PersistenceUntilUtil.isLoaded(Object o)를 제공하여 프록시 인스턴스의 초기화 여부를 확인할 수 있습니다. 초기화되지 않은 인스턴스에 대해false를 반환합니다.
조회된 엔티티가 실제 엔티티인지 프록시 엔티티인지 확인하고 싶은 경우, 클래스명을 로그로 찍었을 때 클래스명 뒤에
javassist와 같은 단어가 포함되어 있다면 이는 프록시 객체임을 나타냅니다.
JPA는 엔티티의 성격에 따라 즉시 로딩(Eager), 지연 로딩(Lazy) 중에서 로딩 방식을 선택할 수 있는 방법을 제공하고 있습니다.
각 연관관계 매핑 정보에 따른 기본 Fetch 전략은 다음과 같습니다.
| 연관관계 매핑 | 기본 Fetch 전략 |
|---|---|
| @OneToOne | FetchType.EAGER |
| @ManyToOne | FetchType.EAGER |
| @OneToMany | FetchType.LAZY |
| @ManyToMany | FetchType.LAZY |
즉시 로딩 Eager Loading은 엔티티를 조회하는 시점에 연관된 엔티티도 함께 조회하는 방식입니다.
@Entity
public class Member {
private String name;
//@ManyToOne은 기본 전략이 Eager라서 명시하지 않아도 됨.
@ManyToOne(fetch = FetchType.EAGER)
private Group group;
}
위와 같이 즉시 로딩으로 지정하면 Member 엔티티를 조회하는 시점에 Group 엔티티도 함께 조회됩니다.
SELCT Member, SELECT Group으로 두 번 조회할 것 처럼 생각되지만 가능한 상황에서JOIN을 사용해서 조회 쿼리를 한 번 만 호출하도록 내부적으로 최적화 되어 있습니다.
위와 같은 예제에서 Group은
nullable = true(기본값)로 지정되어 있기 때문에JOIN최적화 시 외부 조인 방식을 이용하고 있습니다.
하지만외부 조인보다는내부 조인방식이 성능이 더 좋습니다. 그래서 즉시 로딩 시 내부 조인을 이용하고 싶게 만든다면 다음과 같이nullable = false제약 조건을 추가합니다.@ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "g_id", nullable = false) private Group group; //또는 @ManyToOne(fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "g_id") private Group group;
컬렉션에 즉시 로딩을 도입할 때는 다음과 같은 주의사항이 있습니다.
둘 이상의 컬렉션을 즉시 로딩하는 것은 권장하지 않음
컬렉션 조인은 일대다 조인을 수행하게 됩니다. 부모 테이블 P에 대하여 자식 테이블 c1, c2를 조인하게 되면 c1 * c2 만큼의 중복 데이터 조회가 발생하여 애플리케이션의 성능 하락을 발생시킵니다.
주로 사용되는
Hibernate(JPA 인터페이스 구현체)는 하나의 쿼리에서 둘 이상의 컬렉션을 조인하는 경우Hibernate MultipleBagFetchException예외를 발생시키도록 하고 있습니다.
컬렉션 즉시 로딩은 항상 외부 조인을 사용
Member -> Group 다대일 관계에서 NOT NULL 제약조건이 있으면 Member는 항상 Group에 소속되기에 내부 조인을 사용해도 상관없지만 반대로 Group -> Member 일대다 관계를 조인하게 되면 Member가 한 명도 없는 Group은 조회되지 않습니다.
이런 경우로 인해 JPA는 일대다 관계를 즉시 로딩할 때 반드시 외부 조인만을 사용하게 됩니다.
지연 로딩 Lazy Loading은 연관관계의 엔티티를 실제 사용하는 시점에 조회하는 방식입니다.
@Entity
public class Member {
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Group group;
}
지연 로딩으로 설정되면 상기한 프록시 객체의 동작 원리에 따라서 동작하게 됩니다.
기본적으로 각 연관관계마다 기본값으로 설정된 로딩 방식이 있습니다.
하지만 최적화를 위해서는 모든 연관관계에 대해 지연 로딩을 쓰고 필요한 부분에서만 즉시 로딩을 하게 만드는 방법이 권장되고 있습니다.