|
| 1 | +# |
| 2 | + |
| 3 | +## Spring Data JPA의 Paging |
| 4 | + |
| 5 | +Spring Data JPA는 데이터 조회 결과를 효율적으로 관리하기 위해 **Paging**(페이징) 기능을 제공합니다. 페이징은 대량의 데이터를 페이지 단위로 나누어 클라이언트에 반환하는 기술로, **Page**와 **Slice**라는 두 가지 주요 개념이 있습니다. |
| 6 | + |
| 7 | +### Page |
| 8 | + |
| 9 | +Page는 페이징의 전체 정보를 포함하는 데이터 구조입니다. 페이징 처리 시 요청된 페이지의 데이터뿐만 아니라, **전체 페이지 수**, **전체 데이터 수** 등의 메타데이터를 포함합니다. |
| 10 | + |
| 11 | +**특징** |
| 12 | +* 요청한 페이지의 데이터 목록과 함께 페이징 정보를 포함. |
| 13 | +* Page 객체는 데이터 외에 추가적인 페이징 정보도 함께 반환합니다. |
| 14 | +* 데이터 총 개수를 알아야 하므로 COUNT **쿼리를 추가 실행**합니다. |
| 15 | + |
| 16 | +**Page 인터페이스 주요 메서드** |
| 17 | +* `List<T> getContent()`: 현재 페이지에 포함된 데이터. |
| 18 | +* `int getNumber()`: 현재 페이지 번호 (0-based). |
| 19 | +* `int getSize()`: 요청된 페이지 크기. |
| 20 | +* `int getTotalPages()`: 전체 페이지 수. |
| 21 | +* `long getTotalElements()`: 전체 데이터 개수. |
| 22 | +* `boolean isFirst()`: 첫 번째 페이지 여부. |
| 23 | +* `boolean isLast()`: 마지막 페이지 여부. |
| 24 | +* `boolean hasNext()`: 다음 페이지가 있는지 여부. |
| 25 | +* `boolean hasPrevious()`: 이전 페이지가 있는지 여부. |
| 26 | + |
| 27 | +**사용예제** |
| 28 | +```java |
| 29 | +import org.springframework.data.domain.Page; |
| 30 | +import org.springframework.data.domain.PageRequest; |
| 31 | +import org.springframework.data.domain.Pageable; |
| 32 | + |
| 33 | +Pageable pageable = PageRequest.of(0, 10); // 첫 번째 페이지(0), 페이지 크기 10 |
| 34 | +Page<Member> members = memberRepository.findAll(pageable); |
| 35 | + |
| 36 | +System.out.println("Total Elements: " + members.getTotalElements()); // 전체 데이터 개수 |
| 37 | +System.out.println("Total Pages: " + members.getTotalPages()); // 전체 페이지 수 |
| 38 | +System.out.println("Current Page: " + members.getNumber()); // 현재 페이지 번호 |
| 39 | +System.out.println("Page Size: " + members.getSize()); // 페이지 크기 |
| 40 | + |
| 41 | +List<Member> content = members.getContent(); // 현재 페이지 데이터 |
| 42 | + |
| 43 | +``` |
| 44 | +⠀ |
| 45 | +해당 데이터들을 클라이언트에게 전달해주면 됩니다. 필요한 데이터만 뽑아서 주는 것이 깔끔할 것이라고 생각은 합니다. 핵심은 Spring Data JPA가 위의 기능들을 제공해준다는 것입니다. |
| 46 | + |
| 47 | + |
| 48 | +### Slice |
| 49 | + |
| 50 | +Slice는 Page와 유사하지만, **전체 페이지 수나 데이터 총 개수를 계산하지 않는** 데이터 구조입니다. 필요한 데이터만 조회하여 클라이언트에 반환하므로, 성능이 중요한 경우 유리합니다. |
| 51 | + |
| 52 | +**특징** |
| 53 | +* 현재 페이지와 다음 페이지의 존재 여부 정보만 제공. |
| 54 | +* COUNT **쿼리를 실행하지 않아** 성능이 더 우수. |
| 55 | +* 전체 데이터 수나 전체 페이지 수는 알 수 없음. |
| 56 | +* **무한 스크롤**이나 **다음 페이지 요청**이 필요한 상황에서 사용. |
| 57 | + |
| 58 | +**Slice 인터페이스 주요 메서드** |
| 59 | +* List<T> getContent(): 현재 페이지에 포함된 데이터. |
| 60 | +* int getNumber(): 현재 페이지 번호 (0-based). |
| 61 | +* int getSize(): 요청된 페이지 크기. |
| 62 | +* boolean isFirst(): 첫 번째 페이지 여부. |
| 63 | +* boolean hasNext(): 다음 페이지가 있는지 여부. |
| 64 | + |
| 65 | +```java |
| 66 | +import org.springframework.data.domain.PageRequest; |
| 67 | +import org.springframework.data.domain.Pageable; |
| 68 | +import org.springframework.data.domain.Slice; |
| 69 | + |
| 70 | +Pageable pageable = PageRequest.of(0, 10); // 첫 번째 페이지(0), 페이지 크기 10 |
| 71 | +Slice<Member> members = memberRepository.findAllByStatus("ACTIVE", pageable); |
| 72 | + |
| 73 | +System.out.println("Current Page: " + members.getNumber()); // 현재 페이지 번호 |
| 74 | +System.out.println("Page Size: " + members.getSize()); // 페이지 크기 |
| 75 | +System.out.println("Has Next: " + members.hasNext()); // 다음 페이지 여부 |
| 76 | + |
| 77 | +List<Member> content = members.getContent(); // 현재 페이지 데이터 |
| 78 | + |
| 79 | +``` |
| 80 | + |
| 81 | +| **특징** | **Page** | **Slice** | |
| 82 | +|----------------------|------------------------------------|-------------------------------------| |
| 83 | +| **전체 데이터 수** | 제공 (`getTotalElements()`) | 제공하지 않음 | |
| 84 | +| **전체 페이지 수** | 제공 (`getTotalPages()`) | 제공하지 않음 | |
| 85 | +| **성능** | 상대적으로 느림 (`COUNT` 필요) | 상대적으로 빠름 (`COUNT` 필요 없음) | |
| 86 | +| **사용 상황** | 일반적인 페이징 | 무한 스크롤, 간단한 페이지 네비게이션 | |
| 87 | + |
| 88 | + |
| 89 | +**Page vs Slice** |
| 90 | +* Page는 **전체 데이터 정보**를 제공하며 일반적인 페이징에 적합. |
| 91 | +* Slice는 **성능 최적화**와 무한 스크롤에 적합. |
| 92 | + |
| 93 | + |
| 94 | +--- |
| 95 | +## 객체 그래프 탐색 |
| 96 | + |
| 97 | +**객체 그래프 탐색**은 JPA에서 엔티티 간의 연관 관계를 탐색하며 데이터를 가져오는 작업을 의미합니다. JPA에서는 @OneToOne, @OneToMany, @ManyToOne, @ManyToMany 등의 연관 관계 매핑을 통해 객체 간 관계를 설정합니다. |
| 98 | + |
| 99 | +### 객체 그래프 탐색 전략 |
| 100 | + |
| 101 | +**즉시 로딩 (Eager Loading)** |
| 102 | +- 연관된 엔티티를 **즉시 로드**. |
| 103 | +- 연관된 엔티티를 **JOIN 쿼리**로 한 번에 가져옴. |
| 104 | +* 사용: @OneToOne, @ManyToOne 기본값. |
| 105 | + |
| 106 | +```java |
| 107 | +@ManyToOne(fetch = FetchType.EAGER) |
| 108 | +@JoinColumn(name = "member_id") |
| 109 | +private Member member; |
| 110 | + |
| 111 | +``` |
| 112 | + |
| 113 | +**장점** |
| 114 | +* 연관 데이터를 미리 가져와서 지연 로딩 문제 해결 |
| 115 | + |
| 116 | +**단점** |
| 117 | +* 불필요한 데이터를 미리 가져오면 성능 저하 가능 |
| 118 | +* 쿼리가 예측하기 힘들어짐 |
| 119 | + |
| 120 | + |
| 121 | +**지연 로딩 (Lazy Loading)** |
| 122 | +* 연관된 엔티티를 **실제 사용하는 시점**에 로드. |
| 123 | +* 기본값: @OneToMany, @ManyToMany. |
| 124 | + |
| 125 | +```java |
| 126 | +@OneToMany(mappedBy = "member", fetch = FetchType.LAZY) |
| 127 | +private List<Order> orders; |
| 128 | + |
| 129 | +``` |
| 130 | + |
| 131 | +**장점** |
| 132 | +* 필요한 데이터를 사용할 때만 가져옴. |
| 133 | +* 메모리 및 성능 효율성. |
| 134 | + |
| 135 | +⠀**단점** |
| 136 | +* 실제 데이터 접근 시 추가 쿼리 발생. |
| 137 | + |
| 138 | +**N+1 문제** |
| 139 | +* 즉시 로딩 시 **연관된 데이터의 개수만큼 추가적인 SELECT 쿼리가 발생**. |
| 140 | + |
| 141 | +예 |
| 142 | +```sql |
| 143 | +SELECT * FROM member; |
| 144 | +SELECT * FROM orders WHERE member_id = 1; |
| 145 | +SELECT * FROM orders WHERE member_id = 2; |
| 146 | + |
| 147 | +``` |
| 148 | + |
| 149 | + |
| 150 | +해결 방법 |
| 151 | + |
| 152 | +**Fetch Join** |
| 153 | +```java |
| 154 | +@Query("SELECT m FROM Member m JOIN FETCH m.orders") |
| 155 | +List<Member> findAllWithOrders(); |
| 156 | +``` |
| 157 | + |
| 158 | +**EntityGraph** |
| 159 | +```java |
| 160 | +@EntityGraph(attributePaths = {"orders"}) |
| 161 | +List<Member> findAllWithOrders(); |
| 162 | +``` |
| 163 | + |
| 164 | + |
0 commit comments