N+1 문제

박상준·2022년 8월 21일
1

면접지식

목록 보기
10/32
post-custom-banner

N+1 문제란?

연관 관계가 설정된 엩티티를 조회할 경우 조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 현상

@Entity
@Table(name = "user")
@ApiModel("회원엔티티")
public class User extends BaseTimeEntity {
      
      /**
       * 회원번호
       */
      @Id
      @Column(name = "user_no", nullable = false)
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @ApiModelProperty(value = "유저번호", required = true)
      private Long userNo;
      
      /**
       * 이메일
       */
      @Column(name = "email", length = 50, columnDefinition = "VARCHAR(50)")
      @ApiModelProperty(value = "이메일")
      private String email;
      
      /**
       * 비밀번호
       */
      @Column(name = "password", length = 500, columnDefinition = "VARCHAR(500)")
      @ApiModelProperty(value = "비밀번호")
      private String password;
      
      /**
       * 생년월일
       */
      @Column(name = "birth", length = 10, columnDefinition = "VARCHAR(20)")
      private String birth;
      
      /**
       * 닉네임
       */
      @Column(name = "nickname", length = 50, columnDefinition = "NVARCHAR(50)")
      private String nickname;
      
      /**
       * 이용약관동의
       */
      @Column(name = "agree_tos", columnDefinition = "varchar(5) default '0'")
      private String agreeTos;
      
      /**
       * 개인정보수집 및 이용동의
       */
      @Column(name = "agree_picu", columnDefinition = "varchar(5) default '0'")
      private String agreePicu;
      
      /**
       * 이벤트, 프로모션 메일, SMS수신
       */
      @Column(name = "agree_promotion", columnDefinition = "varchar(5) default '0'")
      private String agreePromotion;
      
      /**
       * 성별
       */
      @Column(name = "gender", columnDefinition = "varchar(5) default 'm'")
      private String gender;
      
      /**
       * 역할구분(구매자,판매자,관리자)
       */
      @Column(name = "role", columnDefinition = "varchar(20) default 'b'")
      @Enumerated(EnumType.STRING)
      private Role role;
      
      /**
       * 회원상태
       */
      @Column(name = "del_yn", columnDefinition = "NVARCHAR(5) DEFAULT 'n'")
      private String delYn;
      
      /**
       * 휴대폰 번호
       */
      @Column(name = "phone_nm", nullable = false, columnDefinition = "VARCHAR(20)")
      private String phoneNm;
      
      @ApiModelProperty(value = "장바구니")
      @OneToOne(fetch = LAZY, mappedBy = "user")
      @ToString.Exclude // 연관관계의 주인이 아닌 객체를 mappedBy
      private Cart cart;
      
      @ApiModelProperty(value = "배송지")
      @OneToMany(fetch = LAZY, mappedBy = "user")
      @ToString.Exclude
      private List<Delivery> deliveryList = new ArrayList<>();
      
      @ApiModelProperty(value = "주문목록")
      @OneToMany(fetch = LAZY, mappedBy = "user")
      @ToString.Exclude
      private List<Order> orderList = new ArrayList<>();
      
      @OneToMany(fetch = LAZY, mappedBy = "user")
      @ToString.Exclude
      private List<Review> reviewList = new ArrayList<>();
      
      @OneToOne(fetch = LAZY, mappedBy = "user")
      @ToString.Exclude
      private Jjim jjim;
}

Fetch 모드를 EAGER(즉시 로딩)으로 한 경우

  1. findAll() 을 한 순간 > select u from User u; 이라는 JPQL 구문이 생성
    해당 구문을 분석한 후 select * from User 이라는 SQL 이 생성되어 실행
  2. DB의 결과를 받아 User 엔티티의 인스턴스들을 생성
  3. User 와 연관된 order..등도 로딩해야함
  4. 영속성 컨텍스트에서 연관된 order.. 등도 확인
  5. 영속성 컨텍스트에 없으면 2에서 만들어진 User인스턴스들의 개수에 맞게 select * from User where order_id = ? 이라는 SQL 구문이 생성된다. ( N+1발생 )

Fetch 모드를 LAZY(지연 로딩)으로 한 경우

  1. findAll()을 한 순간 > select u from User u; 이라는 JPQL 구문이 생성
    해당 구문을 분석한 select * from user; 이라는 SQL이 생성되어 실행된다.
  2. DB의 결과를 받아 User 엔티티의 인스턴스들을 생성한다.
  3. 코드 중 user 의 order 객체를 사용하려고 하는 시점에 영속성 컨텍스트에서 연관된 order가 있는지 확인한다.
  4. 영속성 컨텍스트에 없다면 2에서 만들어진 user 인스턴스들의 개수에 맞게 select * from
    User where order_id = ? 이라는 SQL 구문이 생성된다.

지연 로딩으로 설정했다고, N+1 문제가 해결될 것 같지만, 결국 영속성 컨텍스트에 존재하지 않는다면 N+1 문제가 발생한다. 그 시점만 달라질 뿐이다.

해결 방안

  1. Fetch Join

    JPQL을 사용해서 데이터를 가져올 때 연관된 데이터까지 같이 가져오도록 하는 방안이다.
    이는 오라클에서 join 시 on절을 통해 정확하게 어떤 데이터를 연관해서 가져올지 설정하는 것과 비슷한 느낌이다.

    public interface TeamRepository extends JpaRepository<Team, Long> {
        @Query("select t from Team t join fetch t.users")
        List<Team> findAllFetchJoin();
    }
    Hibernate: select team0_.id as id1_0_0_, user2_.id as id1_2_1_, team0_.name as name2_0_0_, user2_.first_name as first_na2_2_1_, user2_.last_name as last_nam3_2_1_, user2_.team_id as team_id4_2_1_, users1_.team_id as team_id1_1_0__, users1_.users_id as users_id2_1_0__ from team team0_ inner join team_users users1_ on team0_.id=users1_.team_id inner join user user2_ on users1_.users_id=user2_.id
    ============== N+1 시점 확인용 ===================
  2. Batch Size

    내가 생각하기에 해당 방안이 제일 효율적이라고 생각한다.

    spring:
      jpa:
        properties:
          hibernate:
            default_batch_fetch_size: 1000

    배치페치사이즈를 기본 1000정도로 설정한다.

    Hibernate: select team0_.id as id1_0_, team0_.name as name2_0_ from team team0_
    Hibernate: select users0_.team_id as team_id1_1_1_, users0_.users_id as users_id2_1_1_, user1_.id as id1_2_0_, user1_.first_name as first_na2_2_0_, user1_.last_name as last_nam3_2_0_, user1_.team_id as team_id4_2_0_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id in (?, ?, ?, ?)

    참조하는 객체가 사용될 때 하나씩 조회문을 수행하지 않고
    한 번에 수행하기 위해서 batch_fetch_size 설정을 지정한다

profile
이전 블로그 : https://oth3410.tistory.com/
post-custom-banner

0개의 댓글