티스토리 뷰

Spring Data JPA

JPA N+1 해결법

LuxuryCoding 2022. 2. 6. 23:52
728x90

JPA N+1? 

Jpa + Spring Boot를 통해 개발을 하다 보면 N+1문제를 마주칠 겁니다. 많이 다루는 Join Fetch, @EntityGraph를 통하여 해결하는데 어떻게 사용하고 단점은 무엇인지 정리해보겠습니다.

❗ 단방향 매핑 형태로  @ManyToOne인 한정된 예제로 진행하기 때문에 그외에 일어나는 해결방법은 다루지 않겠습니다. N+1문제는 이런 식으로 해결하는구나~ 정도만 이해할 수 있는 글입니다.

N+1문제를 정리하기전에 테이블 구조를 통해 설명할게요

아래와 같이 member, board, category 테이블이 있습니다.

 

 JAVA Code로 구현 해보겠습니다.

 

Memeber 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "member")
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String userName;

    @Builder
    public Member(String userName) {
        this.userName = userName;
    }

}

Board

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "board")
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "board_id")
    private Long id;

    private String content;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_Id")
    private CategoryType categoryType;


    @Builder
    public Board(String content, Member member,CategoryType categoryType) {
        this.content = content;
        this.member = member;
        this.categoryType=categoryType;
    }
}

Category

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "category")
public class CategoryType {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "catagory_id")
    private Long id;

    private String categoryName;

    @Builder
    public CategoryType(String categoryName) {
        this.categoryName=categoryName;
    }
}

 

 

👉 그럼 만약 Board 리스트를 조회하려고 하는데 그 안에 아래와 같이 카테고리 이름과 작성자를 넣는 요구 사항을 받았다고 가정하겠습니다.

  {
            "id"1,
            "writer""member1",
            "categoryName""익명",
            "content""content1"
        },
        {
            "id"2,
            "writer""member2",
            "categoryName""자유",
            "content""content2"
        },
        {
            "id"3,
            "writer""member3",
            "categoryName""비밀",
            "content""content3"
        },
}

만들어둔 INSERT 쿼리를 실행 후 Board 전체 리스트를 조회하겠습니다. 

INSERT INTO member (user_name) VALUES ('member1');
INSERT INTO member (user_name) VALUES ('member2');
INSERT INTO member (user_name) VALUES ('member3');

INSERT INTO category (category_name) VALUES ('익명');
INSERT INTO category (category_name) VALUES ('자유');
INSERT INTO category (category_name) VALUES ('비밀');

INSERT INTO board (content,member_id,category_id) VALUES ('content1',1,1);
INSERT INTO board (content,member_id,category_id) VALUES ('content2',2,2);
INSERT INTO board (content,member_id,category_id) VALUES ('content3',3,3);

 

Board 리스트를 가져오는 api를 한번 호출했는데 7번의 쿼리가 나가는 현상을 볼 수 있습니다. 

이것이 N+1이다

이현상이 일어나는 이유는 board에서 매핑된 user와 category를 조회하기 때문입니다. 

 쿼리 실행 개수가  7개인 이유는 

1. board 리스트를 조회하는 쿼리 하나!

2. 제가 INSERT meber를 3개 했고

3. INSERT category 3개

1+3+3 = 7 그래서 7개가 뜬 겁니다. 

 

JPA는 이런 식으로 하위 엔티티들을 첫 쿼리 실행 시 깔끔하게 한 번에 가져오지 않고  Lazy Loading으로 요구사항과 같이 member category가 사용되어 쿼리가 실행될 때 발생하는 문제가 N+1 쿼리 문제입니다. 

그렇다면 member 만개 category 만개 가 있으면 20001번 뜨는 성능 이슈가 발생하게 됩니다.

 

✅ 해결방법

1. join fetch 

Select api 호출 시 가져오고 싶은 필드 join fetch b.member join fetch b.category로 하면 됩니다.

@Query("select b from Board b join fetch b.member join fetch b.category")
List<Board> findAllJoinFetch();

쿼리가 1번으로 줄은 것을 확인할 수 있습니다.

 

2. @EntityGraph

attributePaths에 다음과 같이 필드명을 지정하면 Lazy가 아닌 Eager 조회로 가져오게 됩니다.

@EntityGraph(attributePaths = {"member", "category"})
@Query("select b from Board b")
List<Board> findAllJoinFetch();

@EnitityGraph도 마찬가지로 쿼리가 1번으로 줄은 것을 확인 할 수 있습니다.

 

😊 결론

제가 진행하는 프로젝트에서는 단방향 ManyToOne 형태로 진행했기 때문에 저에게 맞는 해결법을 포스팅하게 되었습니다. 이밖에도 @OneToMany , @ManyToOne 양방향 형태에서 처리하는 N+1문제(Set, Distinct 사용),

현재는 2개의 부모 테이블을 가지고 있지만 (ToOne) 2개의 자식 테이블을 페치 조인한다면 (ToMany)등 때에 따라 약간의 해결법이 다르다는 것을 JPA 공부를 하면서 알게 되었습니다. 결론적으로는 JPA를 사용할 때는 N+1 문제를 항상 염두에 두고 성능 최적화를 생각해야 한다는 점인 것 같습니다.  

 

이걸 보셨다면 추가적으로 

다양한 N+1문제를 공부하시는 방법을 추천드리고 싶습니다

1. 페이징 처리 시  join fetch --> https://bcp0109.tistory.com/304

2. @OntToMany 필드가 두 개일 때 join fetch  --> https://jojoldu.tistory.com/457

 

'Spring Data JPA' 카테고리의 다른 글

더티 체킹 (Dirty Checking) -> JPA Update  (0) 2022.03.02
JDBC VS Mybatis VS JPA  (0) 2021.12.13
댓글
최근에 달린 댓글
최근에 올라온 글
Total
Today
Yesterday
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30