Jpa n+1 문제 해결방법

Simple is Best·2021년 6월 13일
0

Jpa

목록 보기
5/7

source 는 Github 에 있습니다.

Jpa n+1 문제

  • 상위 엔티티에서 하위 엔티티를 조회할 때마다 하위 엔티티 갯수만큼 쿼리가 더 수행되는 현상입니다.
  • 다음은 jpa n + 1 관련 예시입니다.
    • 아래 예시는 Member : Account = 1 : n 관계입니다.
    • Member 데이터가 2개 있고, Member 데이터 1개당 Account 데이터가 2개 있다고 가정을 해보겠습니다.
    • 아래와 같이 MemberService 에서 member 를 가져와서 account 를 조회하면 n+1 문제가 발생합니다.
    • 이는 Fetch type 이 lazy, eager 관계 없습니다.
    • MemberService 를 호출하면 Member 를 조회하는 쿼리 1개, Account 를 조회하는 쿼리 2개가 발생합니다.
  • 아래 예시처럼 n+1 문제가 데이터가 많을 때는 하위 엔티티 만큼 쿼리가 발생하게 되니 문제가 발생할 수 있습니다.

@Slf4j
@RequiredArgsConstructor
@Service
public class MemberService {

    private final MemberRepository memberRepository;

    @Transactional(readOnly = true)
    public List<Member> getMembers2() {
        List<Member> members = memberRepository.findAll();
        members.forEach(member -> {
            log.info("member : {}, account.size : {}", member, member.getAccounts().size());
        });
        return members;
    }
}


@Entity
@Getter
@NoArgsConstructor
public class Member {

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

    @Column
    private String name;

    @Column
    private String telNo;

    @Column
    private int age;

    @JsonIgnore
    @OneToMany(mappedBy = "member")
    private List<Account> accounts = new ArrayList<>();
}


@Entity
@Getter
@NoArgsConstructor
public class Account {

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

    @Column @Setter
    private String accountNo;   // 계좌번호

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
}


n+1 해결하는방법

join fetch

  • join fetch 는 연관된 엔티티나 컬렉션을 한 번에 조회하는 기능입니다.
  • join fetch 를 통해 데이터를 한 번에 읽어오면 n+1 문제는 발생하지 않습니다.
  • 문제는 join fetch 를 통해 여러 하위 엔티티를 조회할 때 MultipleBagFetchException 가 발생하게 됩니다.
  • MultipleBagFetchException 가 발생하는 이유는 다음과 같습니다.
    • Member Entity 가 있다고 가정하고, Member 엔티티는 Account, Car 하위 엔티티를 가지고 있다고 가정하겠습니다.
    • Member : Account = 1 : n , Member : Car = 1: n
    • Member 데이터 1개당 Account, Car 데이터를 2개씩 가지고 있다고 가정하겠습니다.
    • Member 와 account 를 fetch join 하면 데이터를 2개를 가지고 올 것 입니다. (Member + Account)
    • 이 상태에서 Car 를 fetch join 하면 이미 가져온 2개 데이터에서 어떤 기준을 가지고 Car 를 가져와야할지 판단을 할 수 없습니다.
    • 그렇기에 MultipleBagFetchException 발생합니다.

Entity Graph

  • 쉽게 설명하면 조회할 때, 한 번에 조회하는 기능입니다.
  • 그러나 사용하기 좀 어렵습니다.

default_batch_fetch_size 또는 @BatchSize

  • default_batch_fetch_size 는 상위 엔티티 안에 포함돼있는 하위 엔티티를 조회할 때, In Query 로 한 번에 조회하는 기능입니다.
  • 예를 들면, default_batch_fetch_size 가 100 이고, Account, Car 데이터를 조회할 때 최대 100개까지 member_id 를 InQuery 로 가져옵니다.
  • default_batch_fetch_size 는 전역 설정이며, @BatchSize 는 Entity 로 설정할 수 있습니다. 아래 예시에 사용법이 있습니다.

@Entity
public class Member {

    @OneToMany(mappedBy = "member")
    @BatchSize(size = 100)
    private List<Account> accounts = new ArrayList<>();
}


spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

    select
        accounts0_.member_id as member_i3_0_1_,
        accounts0_.account_id as account_1_0_1_,
        accounts0_.account_id as account_1_0_0_,
        accounts0_.account_no as account_2_0_0_,
        accounts0_.member_id as member_i3_0_0_ 
    from
        account accounts0_ 
    where
        accounts0_.member_id in (
            ?, ?
        )

reference

profile
Simple is best

0개의 댓글