[JPA] 즉시 로딩과 지연 로딩 feat (N + 1 문제)

SIK407·2024년 8월 12일
0

spring

목록 보기
6/11
post-thumbnail

일단 이걸 이해할려면 JPA 프록시 이걸 알아야된다.

CS질문에서 많이 나오는 질문이기도 하고....
실무에서도 꼭 알아야 할 개념이기도 하다고 해서, 이번건 아주 중요하다.



FetchType

@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
private List<Member> members = new ArrayList<>();

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

어노테이션을 보면, fetch = FetchType.~ 이란 속성이 있다.

FetchType
JPA가 하나의 Entity를 조회할 때, 연관관계에 있는 객체들을 어떻게 가져올 것이냐를 나타내는 설정값이다.

JPA는 JPQL을 객체와 필드를 보고 쿼리문으로 변환하여 DB에 보낸다.
그럼 다른 테이블과 연관매핑을 하게 되는데, 총 4개의 어노테이션을 사용한다.
@OneToOne : 1대1
@OneToMany : 1대다
@ManyToOne : 다대1
@ManyToMany: 다대다

그럼 이 연관된 테이블 데이터를 한번에 가져올까...? 혹은 한개씩 따로 가져올까...? 를 설정하는게 FetchType이다.

방식은 총 두가지다.

  1. FetchType.EAGER: 즉시 로딩
  2. FetchType.LAZY: 지연로딩


즉시로딩 (FetchType.EAGER)

일단!!

@ManyToOne(다대1)
@OneToOne (일대일)
이 둘은 기본이 즉시 로딩이다.

@Entity
@Getter @Setter
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;
}

Member라는 엔티티는 이렇다.

즉시로딩 EAGER
데이터를 조회할 때, 연관된 모든 객체의 데이터까지 한 번에 불러오는 것

현재 Member는 Team이란 테이블하고 @ManyToOne (다대1)로 매핑이 되어 있는 상태다.

fetch = FetchType.EAGER 로 즉시로딩으로 설정되어 있으니까,
Member를 가져올 때, 그 Team의 데이터를 같이 한 번에 가져오게 된다.

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

그래서 그냥 단순하게 Member를 찾을려고 해도...

Hibernate: 
    select
        m1_0.MEMBER_ID,
        t1_0.TEAM_ID,
        t1_0.name,
        m1_0.USERNAME 
    from
        Member m1_0 
    left join
        Team t1_0 
            on t1_0.TEAM_ID=m1_0.TEAM_ID 
    where
        m1_0.MEMBER_ID=?

저기 보면 left join (왼쪽조인)을 해서 Team의 데이터까지 전부 다 가져오게 된다.



지연로딩 (FetchType.LAZY)

@OneToMany(1대다)
@ManyToMany (다대다)
이 둘은 기본이 지연 로딩이다.

@Entity
@Getter @Setter
public class Team {

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

    private String name;

    @OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
    private List<Member> members = new ArrayList<>();
}

Team의 엔티티는 이렇다.

지연로딩 LAZY
데이터를 조회할 때, 필요한 시점에 연관된 객체의 데이터를 불러오는 것이다.

현재 Team은 Member란 테이블하고 @OneToMany (1대다)로 매핑이 되어 있는 상태다.

fetch = FetchType.LAZY 로 지연로딩으로 설정되어 있으니까,
Team을 가져올 때, 정말로 그 Team의 데이터만 일단 가져오게 된다.

Team findTeam = em.find(Team.class, 1L);

이런식으로 팀을 일단 찾아오면.....

Hibernate: 
    select
        t1_0.TEAM_ID,
        t1_0.name 
    from
        Team t1_0 
    where
        t1_0.TEAM_ID=?

이런 쿼리를 만들고 보낸다.
지금 이 쿼리 만으로는 속해있는 Member의 정보를 알 수 없다.

List<Member> members = findTeam.getMembers();

for (Member m : members) {
    System.out.println("m = " + m.getUsername());
}

그럼 이렇게 속해있는 Member를 찾을려고 선언하면....?

Hibernate: 
    select
        m1_0.TEAM_ID,
        m1_0.MEMBER_ID,
        m1_0.USERNAME 
    from
        Member m1_0 
    where
        m1_0.TEAM_ID=?

이런 쿼리를 만들어서 보낸다.

"어....? 그럼 Team 정보는 어떤 방식으로 들어와요...?"

그래서 맨 위에 JPA 프록시 이 개념을 먼저 알고 와야 편하다는 뜻 이었다.

현재 Team의 Entity는 프록시 객체로 들어와 있는 상태다.
그리고 Team에 대한 정보를 찾을려고 하면, 쿼리가 발생한다.



주의! N+1 문제...

근데 여러 블로그에서도 그렇고, 내가 듣고 있는 강의에서도 강사님께서 이런 말씀을 하셨다.

"가급적 지연 로딩만 사용(특히 실무에서)"

왜 다들 이렇게 말할까?
여려가지 이유가 있다.

1. 효율적인 통신

위에 엔티티들 중, 즉시로딩이라고 가정하자.
난 Member만 가져와서 일단 사용해야 되는데, 쓸데없는 Team의 데이터까지 가져오게 된다.

그럼 쓸데없이 더 큰 데이터를 가져오게 된다.

그러니까 일단 디폴트로 지연로딩(LAZY)로 다들 사용하라는 거다.

2. N+1 문제

물론 난 면접을 안다녀서 모르는데.....
CS질문으로 많이 나오는 듯 하다.

N+1 문제
내가 생각한 조회 쿼리만 나가야 하는데, 나오지 않아도 되는 조회 쿼리가 N개가 더 발생

흠... 뭔가 이것만 보면 이해가 될듯 말듯 싶은데....
한번 예시로 바로 보자.

Id이름소속 팀
1LMember1TeamA
2LMember2TeamA
3LMember3TeamB
4LMember4TeamB

Id팀 이름
1LTeamA
2LTeamB

Member와 Team에 이런 데이터가 있다고 가정하자.
물론 현재는 즉시로딩(EAGER)로 설정한 상태다.

List<Member> findMember1 = em.createQuery("select m from Member m join m.team t", Member.class)
                    .getResultList();
                    
// SELECT * FROM member JOIN team t ON t.id = m.team

그럼 이런 쿼리들이 보내진다.

Hibernate: 
    /* select
        m 
    from
        Member m 
    join
        m.team t */ select
            m1_0.MEMBER_ID,
            m1_0.team_TEAM_ID,
            m1_0.USERNAME 
        from
            Member m1_0 
        join
            Team t1_0 
                on t1_0.TEAM_ID=m1_0.team_TEAM_ID
Hibernate: 
    select
        t1_0.TEAM_ID,
        t1_0.name 
    from
        Team t1_0 
    where
        t1_0.TEAM_ID=?
Hibernate: 
    select
        t1_0.TEAM_ID,
        t1_0.name 
    from
        Team t1_0 
    where
        t1_0.TEAM_ID=?
        
        
1. SELECT * FROM member JOIN team t ON t.id = m.team
2. SELECT * FROM team t WHERE t.id = 1
3. SELECT * FROM team t WHERE t.id = 2

이렇게 총 3개의 쿼리가 나간다.
난 그냥 Member 목록들을 조회하고 싶었을 뿐인데... 왜 이렇게 나가냐면

Member들을 조회하다 보니까, 속해있는 Team의 종류가 총 두개다.
그래서 각각 Team을 또 쿼리를 만들어서 각각 조회를 한거다.

그러니까 Team의 갯수가 N이 되는거고, 내가 Member 목록들을 조회한 쿼리가 한개니까
N+1개의 쿼리가 만들어지는거다...

이 N+1의 문제를 해결하는 방법은 간단하게 지연 로딩으로 설정하면 된다.

그래서 실무에서는 무조건!!! 기본으로 LAZY로 설정하라는 뜻이였다.



또 다른 N+1 해결법

근데, 지연로딩도 N+1 문제를 만든다.
나중에 그 멤버 조회하면 또 쿼리를 만들어서 던진다.

1. fetch join

나중에 한번 블로그를 더 쓸것 같지만, 여기서 일단 알아보자.

List<Member> findMember1 = 
	em.createQuery("select m from Member m join fetch m.team t", Member.class)
                    .getResultList();

이런식으로 하면 된다.

2. Batch Size 사용

JPA에는 Batch Size라는 기능이 있다.
JPA에서 지연로딩을 할 때, 한번에 최대 Batch Size만큼의 Entity를 where절에 "in"으로 가져온다.

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

이런식으로 전체 다 설정해주거나 (Spring Boot 한정)

@ManyToOne(fetch = FetchType.LAZY)
@BatchSize(size = 100)
private Team team;

이런식으로 한 매핑 관계에서만 어노테이션으로 적용해도 된다.
이 방식은 지연 로딩 방식에서 사용하는게 일반적이다.

근데 이 BatchSize를 너무 작게 잡으면, 그만큼 더 쿼리를 날리게 되고,
너무 크게 잡으면 메모리를 많이 잡아먹는다.

일반적으로 100 ~ 1000개 사이로 잡는듯 싶다.

profile
Spring 백엔드!

0개의 댓글

관련 채용 정보