본문 바로가기
[ ORM ]/JPA

[ JPA ] 즉시 로딩과 지연 로딩(FetchType.LAZY or EAGER)

by 환이s 2024. 3. 26.


Intro

 

이전 포스팅에서 프록시(Proxy)에 대한 글을 작성했습니다.

 

프록시(Proxy)는 이번 포스팅에서 즉시 로딩(EAGER)지연 로딩(LAZY)을 구현하는데 중요한 개념인데,

일단 원리는 미뤄두고 즉시 로딩(EAGER)지연 로딩(LAZY)에 대한 개념에 대해 먼저 알아보자.

 

 

예제 테이블로 Member와 Team이 있다고 가정해보자.

Member를 조회할 때 Team도 함께 조회해야 할까?

 

비즈니스 로직에서 단순히 멤버 로직만 사용하는데 함께 조회하면,

아무리 연관관계가 걸려 있다고 해도 손해이다.

 

JPA에서는 데이터를 조회할 때 즉시 로딩(EAGER)지연 로딩(LAZY) 두 가지 방식이 있다.

 

이 두 가지 방식을 간단하게 설명하면

즉시 로딩(EAGER)데이터를 조회할 때 연관된 데이터까지 한 번에 불러오는 로딩이고,

지연 로딩(LAZY)필요한 시점에 연관된 데이터를 불러오는 로딩이라고 할 수 있다.

 

위에서 언급했듯 Member 테이블을 조회할 때 Team 테이블도 함께 조회하는 건 불필요하고

실무에서는 테이블이 1~2개가 아닌, 50개 이상을 보유한 곳이 대다수이다.

 

그렇다는 건 성능 문제로도 갈 수 있다는 건데,

JPA는 이 문제를 지연로딩 LAZY를 사용해서 프록시로 조회하는 방법으로 해결 한다.

 

지연로딩? 즉시로딩? 이론적인 부분만 생각하면 조회할 때 늦게 호출하고 바로 호출하는 개념으로 생각하면 쉽다.

하지만 아직 상황에 맞게 사용하기엔 헷갈리고 자신있게 코드 구현을 하기엔 부족하다.

 

바로 예제 코드로 알아보자.


즉시로딩 - FetchType.EAGER

 

먼저 예제 코드로 사용되는 Member , Team 엔티티 코드를 확인해보자.

 

// Member
@Entity
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

// Team
@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

 

위 예제 코드를 풀어보면 Member와 Team은 양방향 연관관계이고,

연관관계의 주인은 Member 엔티티이다. 

(이 부분에서 이해가 안되시는 분들은 아래 글을 먼저 읽고 오는 걸 추천한다.)

 

[ JPA ] 연관관계 매핑 기초

연관관계 매핑? 연관관계 매핑이란 객체의 참조와 테이블의 외래 키를 매핑하는 것을 의미한다. JPA에서는 연관 관계에 있는 상대 테이블의 PK를 멤버 변수로 갖지 않고, 엔티티 객체 자체를 통째

drg2524.tistory.com

 

예제 코드에서 Team 컬렉션에 @ManyToOne(fetch = FetchType.EAGER) 설정을 했는데,

@ManyToOne 애노테이션은 Default FetchType.EAGER라서 생략해도 동작한다.(아래 참고 박스 확인!)

 

다음으로는 확인을 위한 간단한 데이터를 추가해보자.

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);

em.flush();
em.clear();

Member findMember = em.find(Member.class, member.getId());

 

코드만 봐도 정말 단순하게 코드를 작성했고 위 코드를 보면 Team 객체와 Member 객체를 각각 만들고

Member 객체의 Setter 메서드를 통해 team 객체를 셋팅했다.

 

그리고 find() 메서드를 통해 Member 테이블을 조회한다.

 

결과 로그를 확인해보자.

Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_0_0_,
        member0_.TEAM_ID as TEAM_ID3_0_0_,
        member0_.USERNAME as USERNAME2_0_0_,
        team1_.TEAM_ID as TEAM_ID1_1_1_,
        team1_.name as name2_1_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?

 

위 로그를 통해 Query를 확인해보면

즉시 로딩(EAGER) Member를 조회하는 시점바로 Team까지 불러오는 JOIN Query를 통해서 한꺼번에 데이터를 불러오는 것을 볼 수 있다.

 

그렇다면 즉시 로딩(EAGER)을 사용하면 너무 편리한 거 아닌가?

 

물론 편리하다.

하지만 즉시 로딩(EAGER)가급적이면 사용하지 않는 걸 권장한다.

특히, 실무에서 즉시 로딩(EAGER)을 사용하면 큰 데미지를 입을 수 있다.

 

예제 코드로는 Member와 연관된 Team이 1개여서 Team을 조회하는 쿼리가 1개가 나갔지만

만약 Member를 조회하는 Query를 작성해서 Team도 같이 호출되었을 때,

 

예제 처럼 1개가 아닌 1000~2000개라면..?

Member를 조회하는 Query는 한 번 요청했는데, team을 조회하는 Query는 1000~2000개가 추가로 나가게 된다.

그럼 당연히 데미지가 생길 수 있고, 속도 개선이 필요한 기능으로 뽑힌다.

(만약 내가 그 코드의 유지 보수 인원으로 지정되었을 때 정말 고통에 빠질 수 있다.)

 

그렇기 때문에 가급적이면 기본적으로 바로 알아볼 지연 로딩(LAZY)을 사용하는 것이 좋다.


지연 로딩 - FetchType.LAZY

 

위 예제 코드에서 FetchTypeLAZY로 변경하고 실행해서 결과를 확인해보면

 

Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_0_0_,
        member0_.TEAM_ID as TEAM_ID3_0_0_,
        member0_.USERNAME as USERNAME2_0_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?

 

즉시 로딩(EAGER)과는 다르게 Member만 조회해오는 것을 볼 수 있는데,

여기서 추가로 Member 객체로 부터 Team 객체의 데이터를 실질적으로 요구하는 코드를 작성해보면

Member findMember = em.find(Member.class, member.getId());
findMember.getTeam().getName();

 

Hibernate: 
    select
        team0_.TEAM_ID as TEAM_ID1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?

 

호출한 시점을 기준으로 조회하는 걸 확인할 수 있다.

 

좀 더 확실한 이해를 위해 아래 그림과 설명을 참고하자.

 

 

지연 로딩(LAZY) 예제 코드의 내부 메커니즘은 위 그림과 같다.

 

로딩되는 시점에 LAZY 로딩 설정이 되어있는 Team Entity는 프록시 객체로 가져온다.

그리고 실제 객체를 사용하는 시점에(Team을 호출하는 시점) 초기화가 되고, DB에서 Query가 호출된다.

 

정리하자면 위 코드에 .getTeam()으로 Team을 조회하면 프록시 객체가 조회되고,

getTeam().getXXX()으로 팀의 필드에 접근 할 때, Query가 호출된다 .

참고)

Fetch의 디볼트 값은 @xxToOne에서는 EAGER , @xxToMany에서는 LAZY이다.
그렇기에 ManyToOne, OneToOne은 LAZY 설정하려면 필드에서 적용을 시켜줘야 하고
OneToMany, ManyToMany 또한 EAGER 설정하려면 필드에 적용해줘야 한다.

마치며

 

오늘은 이전 포스팅 프록시(Proxy)에 이어서 즉시로딩(EAGER)과 지연로딩(LAZY)에 대해 알아봤습니다.

 

다음 포스팅으로는 값 타입에 대한 개념 및 벨류 타입 사용법에 대해 포스팅을 작성하려고 합니다.

JPA는 Spring만큼 범위가 넓어서 꾸준히 복습을 해주고 직접 코딩을 해보지 않으면 사용하기 다소 어렵습니다

 

다음 포스팅에서 뵙겠습니다.

위 포스팅 글은 김영한님의 자바 ORM 표준 JPA 프로그래밍-기본편을 참고했습니다.

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 강의 - 인프런

현업에서 실제로 JPA로 개발을 하고 있습니다. 그런 입장에서보면 지금 작성하고 있는 코드들이 어떻게 작동하는지 이해하는데 큰 도움을 주는 강의입니다. 다음은 제가 느낀 이 강의의 장점들

www.inflearn.com

 

728x90