본문 바로가기
IT/Spring boot

[Spring boot] JPA FetchType

by kyu-nahc 2024. 9. 19.
반응형

FetchType

FetchType이란 JPA에서 엔티티를 조회할 때 연관된 ‘엔티티 조회 방법을 결정하는 전략’을 의미한다.
javax.persistence.FetchType 패키지 내에 열거형(Eunm) 형태로 제공된다.

// javax.persistence.FetchType.class
public enum FetchType {
    LAZY,
    EAGER;

    private FetchType() {
    }
}

 

FetchType에는 FetchType.EAGER 타입으로 ‘즉시 로딩(Eager Loading)’ 전략과

FetchType.LAZY 타입으로 ‘지연 로딩(Lazy Loading)’ 전략이 존재한다.

FetchType 타입은 언제 사용하는 것일까?
A라는 엔티티와 B라는 엔티티가 존재한다는 가정 하였을 때,

테이블 간의 관계(Join)에서 A 엔티티를 조회하였을 경우에 B 엔티티를 ‘함께 조회’할 것인지

아니면 ‘필요에 따라 조회할 것’인지 결정하는 데 사용이 된다.

먼저 테이블 간의 관계에서 사용하는 어노테이션을 먼저 정리해 보면 다음과 같다.
A와 B라는 엔티티가 존재한다는 가정하에 두 엔티티 간의 관계를 정의하는 어노테이션이다.

어노테이션 DB 매핑 어노테이션 설명
@OneToOne 1:1 관계 매핑 한 엔티티의 필드가 다른 엔티티와 1:1 관계
@OneToMany 1:N 관계 매핑 한 엔티티가 다른 엔티티들과 1:N 관계
@ManyToOne N:1 관계 매핑 한 엔티티가 다른 엔티티와 N:1 관계
@ManyToMany N:N 관계 매핑 한 엔티티가 다른 엔티티들과 N:N 관계

 

이제 예시를 통해 Lazy와 Eager의 차이점을 살펴보면 다음과 같다.

Entity 관계


Member라는 테이블과 Post라는 테이블 간의 1 : N 관계를 형성하고 있다. 

즉, 1개의 사용자는 여러 개의 게시물을 작성할 수 있다는 형태로 설계가 되어 있다.

@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Member extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String userId; // User Id
    private String password; // User password
    private String email; // User Email
    private String name; // User name
    @OneToMany(fetch=FetchType.LAZY, mappedBy = "member")
    @JsonManagedReference
    private List<Post> posts = new ArrayList<>();
}
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Post extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String content;
    @ManyToOne(fetch=FetchType.EAGER)
    @JsonBackReference
    private Member member;
}


이러한 관계는 Member와 Post 간의 관계에서 Member 엔티티 기준으로 OneToMany 관계를 가진다.

해당 관계에서 fetch 속성을 기반으로 FetchType.LAZY 속성값을 지정하였다.
지연 로딩(Lazy Loading)의 관계에서는 Member를 조회할 때 Post는 조회하지 않고,

Post 데이터가 실제로 사용될 때 해당 데이터를 조회한다는 것이다.

이제 즉시 로딩과 지연 로딩의 특징을 살펴보자.

 

 

즉시 로딩 ( FetchType.EAGER )

즉시 로딩이란 엔티티를 조회하는 방법을 정의하는 전략 중 하나로 테이블 간의 관계에서

‘A라는 엔티티를 조회할 때 연관된 B라는 엔티티도 함께 조회하는 방식’을 의미한다.

@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Member extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String userId; // User Id
    private String password; // User password
    private String email; // User Email
    private String name; // User name
    @OneToMany(fetch=FetchType.EAGER, mappedBy = "member")
    @JsonManagedReference
    private List<Post> posts = new ArrayList<>();
}

@Test
void userEagerType(){
    System.out.println("== start ==");
    Member member = memberRepository.findById(1L)
            .orElseThrow(RuntimeException::new);
    System.out.println("== end ==");
    System.out.println(member.getName());
}

 

실제로 Member Entity의 fetch 속성을 즉시 로딩으로 설정하고,

테스트 코드를 통해 Member Entity를 조회하였을 때의 결과는 다음과 같다.

== start ==
Hibernate: 
    select
        m1_0.id,
        m1_0.created_date_time,
        m1_0.email,
        m1_0.modified_date_time,
        m1_0.name,
        m1_0.password,
        m1_0.user_id,
        p1_0.member_id,
        p1_0.id,
        p1_0.content,
        p1_0.created_date_time,
        p1_0.modified_date_time,
        p1_0.title 
    from
        member m1_0 
    left join
        post p1_0 
            on m1_0.id=p1_0.member_id 
    where
        m1_0.id=?
== end ==
username

 

Member는 즉시 로딩(FetchType.EAGER)으로 지정하여서

Member를 호출하는 메서드만 불러도 위와 같은 출력 결과를 보여준다.
즉, @OneToMany인 Member와 @ManyToOne인 Post의 조회 결과로는

LEFT JOIN이 되어서 Member값이 모두 출력되는 조회결과를 얻었다.

이번에는 FetchType을 Eager로 유지하고 findAll()을 통해 모든 Member에 대해 조회한다고 해보자.

@Test
void userEagerTypeAndAllMember(){
    System.out.println("== start ==");
    List<Member> member = memberRepository.findAll();
    System.out.println("== end ==");
}

 

Member는 즉시 로딩(FetchType.EAGER)으로 지정하여서

Member를 호출하는 메서드만 불러도 아래와 같은 출력 형식을 갖게 된다.

== start ==
Hibernate: 
    select
        m1_0.id,
        m1_0.created_date_time,
        m1_0.email,
        m1_0.modified_date_time,
        m1_0.name,
        m1_0.password,
        m1_0.user_id 
    from
        member m1_0
Hibernate: 
    select
        p1_0.member_id,
        p1_0.id,
        p1_0.content,
        p1_0.created_date_time,
        p1_0.modified_date_time,
        p1_0.title 
    from
        post p1_0 
    where
        p1_0.member_id=?
== end ==

 

Member를 select 해왔지만 JPA는 Post에 대해서 EAGER가 걸려있는 것을 보고

Select 한 모든 User에 대해서 Post가 있는지를 검색하게 된다.

즉, 모든 Member에 대해서 검색하고 싶어서 Select 쿼리를 하나 날렸지만,

즉시 로딩이 걸려있기 때문에 각각의 Member가 가진 Post를 모두 검색한 것이 된다.
이러한 방식은 한 번의 쿼리로 필요한 모든 데이터를 가져올 수 있으나

필요하지 않은 데이터까지 조회되어 성능 이슈가 발생할 수 있다.
또한 즉시 로딩의 경우 연관된 ‘엔티티를 실제로 사용될 때 조회하는 방식’이므로

엔티티를 조회할 때 연관된 엔티티도 함께 조회하여 메모리에 로드하는 것이다.

 

 

지연 로딩 ( FetchType.LAZY )

지연 로딩이란 엔티티를 조회하는 방법을 정의한 전략 중 하나로 테이블 간의 관계에서

‘A라는 엔티티를 조회할 때 연관된 B라는 엔티티도 있지만 A라는 엔티티만 조회하는 방식’을 의미한다.

이러한 방식은 한 번에 모든 데이터를 가져오지 않아 불필요한 데이터를 가져오는 것을

방지하여 성능을 향상할 수 있다. 테스트 코드를 통해 차이점을 살펴보면 다음과 같다.

@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Member extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String userId; // User Id
    private String password; // User password
    private String email; // User Email
    private String name; // User name
    @OneToMany(fetch=FetchType.LAZY, mappedBy = "member")
    @JsonManagedReference
    private List<Post> posts = new ArrayList<>();
}
@Test
void userEagerType(){
    System.out.println("== start ==");
    Member member = memberRepository.findById(1L)
            .orElseThrow(RuntimeException::new);
    System.out.println("== end ==");
    System.out.println(member.getName());
}

 

Member에서 기존에 존재하던 즉시 로딩을 지연 로딩(FetchType.LAZY)으로 변형한다.

그 후 마찬가지로 위에서 사용한 동일한 테스트 코드를 사용하였다.

== start ==
Hibernate: 
    select
        m1_0.id,
        m1_0.created_date_time,
        m1_0.email,
        m1_0.modified_date_time,
        m1_0.name,
        m1_0.password,
        m1_0.user_id 
    from
        member m1_0 
    where
        m1_0.id=?
== end ==
username

 

위의 출력을 보면 즉시 로딩과 다르게 PK를 통해 Member를 호출하는 메서드에서

Member에 대한 데이터만 가져오고 Join 연산을 통해 Post 정보까지 가져오지는 않는다.

즉 @OneToMany 관계가 지연 로딩으로 연결되어 있을 경우,

Member 엔티티를 조회할 때 Post 엔티티는 조회하지 않는다.

Post 엔티티는 실제로 사용될 때 데이터베이스에서 조회되는 것이다.

@Test
@Transactional
void userEagerType(){
    System.out.println("== start ==");
    Member member = memberRepository.findById(1L)
            .orElseThrow(RuntimeException::new);
    System.out.println("== end ==");
    System.out.println(member.getPosts().size());
}
== start ==
Hibernate: 
    select
        m1_0.id,
        m1_0.created_date_time,
        m1_0.email,
        m1_0.modified_date_time,
        m1_0.name,
        m1_0.password,
        m1_0.user_id 
    from
        member m1_0 
    where
        m1_0.id=?
== end ==
Hibernate: 
    select
        p1_0.member_id,
        p1_0.id,
        p1_0.content,
        p1_0.created_date_time,
        p1_0.modified_date_time,
        p1_0.title 
    from
        post p1_0 
    where
        p1_0.member_id=?
0

 

다음과 같이 Post 데이터에 접근할 때, Join 연산을 통해 Post 데이터까지 가져오게 된다.

이와 같이 지연 로딩을 사용하면 필요하지 않은 데이터까지 조회되어 성능 이슈가 발생하는 것을

피할 수 있다. 연관된 객체를 "사용"하는 시점에 로딩을 해주는 방법이기 때문에

@OnetoMany 매핑에서 즉시 로딩보다 효율적이라고 할 수 있다.

현재 Fetch type은 default로 ~ToMany에서는 Lazy~ToOne에서는 Eager로 지정되어 있는데

이런 부분들은 default옵션을 사용한다고 하더라도 명시해 주는 것이 좋을 수 있다.

 

 

지연 로딩의 원리

지연 로딩에서는 프록시 패턴(Proxy)을 이용한다.

실제 객체를 대신하여 대리 객체(Proxy Object)를 생성하여 사용하는 디자인 패턴을 의미한다.

프록시 패턴 처리과정 및 구조는 다음과 같다.


Subject(인터페이스), RealSubject(구현체), Proxy(대행자)로 구성이 되어 있다.
SubjectRealSubject, 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 인터페이스의 DoAction()이라는 메서드를 호출한다.
해당 호출에서는 실제 객체를 이용하지 않기에(Lazy Loading) 

이에 따르는 구현체인 RealSubject 클래스의 DoAction() 메서드가 수행되는 것이 아닌 

Proxy가 이를 위임(Delgate) 받아서 DoAction()의 수행처리가 된다.

 

다음으로 프록시 객체(Proxy Object)에 대한 설명이다.
프록시 객체는 실제 객체를 대신하여 사용되는 객체를 의미한다.

실제 객체와 같은 인터페이스를 가지고 있으므로, 실제 객체와 동일하게 사용될 수 있다.
실제 객체의 생성 시점을 제어하는 데 사용되며 이는 지연 로딩(Lazy Loading)의 핵심 원리이다.

실제 객체가 필요하기 전까지 실제 객체의 생성을 지연하며,

실제 객체가 필요한 시점에 실제 객체를 생성하고 초기화한다.

 

JPA에서는 엔티티를 프록시로 로딩하는 기능을 제공한다.

@ManyToOne, @OneToOne 등의 관계에서는 FetchType을 LAZY로 설정하면,

연관된 엔티티를 프록시 객체로 로딩하고, 실제 엔티티가 필요한 시점에 데이터베이스에서 조회한다.
프록시 객체를 이용함으로써, 실제 객체의 생성 시점을 제어하고,

불필요한 데이터베이스 조회를 줄여 성능을 향상할 수 있는 것이다.

이제 지연 로딩에서 바라보는 프록시 패턴/객체를 자세히 살펴보면 다음과 같다.

public class Member extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String userId; // User Id
    private String password; // User password
    private String email; // User Email
    private String name; // User name
    @OneToMany(fetch=FetchType.LAZY, mappedBy = "member")
    private List<Post> posts = new ArrayList<>();
}

 

클라이언트는 현재 Post라는 엔티티를 ‘지연로딩(Lazy Loading)’으로 지정한 상태이다.
Post라는 엔티티를 지연로딩으로 지정을 하게 되면 최초 로드 시 실제 엔티티를 가져오는 것이 아닌

엔티티의 프락시(Proxy)를 먼저 로드를 하게 된다. 지연로딩으로 지정하면 프록시가 자동으로 생기며,

실제 객체가 필요하기 전까지 실제 객체의 생성을 지연하기 위해 된다.

아래 구조도는 Proxy를 통해 실제 객체를 이용하는 방법을 나타낸다.

1. Client  → PostProxy

클라이언트는 Post Entity를 지연로딩 형태로 구성하였기에 PostProxy가 생성이 되었고,

클라이언트는 실제 객체를 사용하기 위해 getTitle()이라는 메서드를 호출한다.

 

2, 3. PostProxy → 영속성 컨텍스트 → DB 조회

getTitle()이라는 메서드는 데이터베이스를 조회하는 처리가 있기에

영속성 컨텍스트를 통해 DB에 접근하여 DB데이터를 조회한다.

 

4. 영속성 컨텍스트 → Post(Entity)

지연되었던 객체 생성 처리가 수행되어 실제 Entity가 생성된다.

 

5. PostProxy → Post(Entity)

PostProxy에서 target으로 가리키고 있는 Post Entity의

getTItle() 메서드를 호출함으로써 지연된 객체의 메서드를 호출한다.

 

 

지연 로딩의 원리는 위와 같고, 프록시 패턴을 통해 구현된다.

지연 로딩을 사용하면 필요하지 않은 데이터까지 조회되어 성능 이슈가 발생하는 것을

피할 수 있고연관된 객체를 사용하는 시점에 로딩을 해주는 방법이기 때문에

@OnetoMany 매핑에서 즉시 로딩보다 효율적이라고 할 수 있다.

 

즉시 로딩은 연관 데이터를 모두 조회하기 때문에, 예상하지 못한 쿼리가 발생할 가능성이 높고,

JPQL에서 N+1 문제를 일으킨다. N+1 문제조회 시 1개의 쿼리를 생각하고 설계를 했으나

나오지 않아도 되는 조회의 쿼리가 N개가 더 발생하는 문제이다.

즉 연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 개수(N) 만큼

연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오기 때문에 발생하는 것이다.

따라서 연관 관계 설정 시 즉시 로딩이 필요한 경우를 제외하고는,

지연 로딩 (Lazy)를 사용하는 것이 더 효율적이라고 할 수 있다.

 

 

 

참고자료

https://adjh54.tistory.com/476

반응형

loading