Spring 무νμ€ν¬λ‘€ ꡬν (1) - 컀μ κΈ°λ° νμ΄μ§λ€μ΄μ
π§ νμ΄μ§λ€μ΄μ μ΄λ?
- μ½ν μΈ λ₯Ό μ¬λ¬ νμ΄μ§λ‘ λλκ³ , μ΄μ νΉμ λ€μ νμ΄μ§λ‘ λμ΄κ°κ±°λ νΉμ νμ΄μ§λ‘ λμ΄κ° μ μλ λ§ν¬λ₯Ό νμ΄μ§ μλ¨μ΄λ νλ¨μ λ°°μΉνλ λ°©λ²
μΌνλͺ° νλ¨, κ²μ κ²°κ³Ό νλ¨μμ μ΅μνκ² μ°Ύμλ³΄μ€ μ μμ΅λλ€.
π§ 무νμ€ν¬λ‘€μ΄λ?
- λΈλΌμ°μ λλ μ€λ§νΈν°μμ μ€ν¬λ‘€ λ§λκ° νλ¨μ λλ¬νλ κ²μ λ°©μ§νλ κ²μ λ§ν©λλ€.
- μ¬μ©μκ° νμ΄μ§λ₯Ό λ μλλ‘ μ€ν¬λ‘€ ν λλ§λ€ μλ‘μ΄ μ½ν μΈ κ° μΆκ°λ©λλ€.
μΈμ€νκ·Έλ¨ νΌλ, μΌνλͺ° μν 리μ€νΈλ₯Ό μλλ‘ μ€ν¬λ‘€νλ€ λ³΄λ©΄ μ κΉμ λ‘λ©μ κ±°μΉκ³ 컨ν μΈ κ° μΆκ°λλ κ²½νμ νμ μ μμ£ ?! 무νμ€ν¬λ‘€μ μ μ©ν κ²½μ°μ λλ€.
π§ 컀μ κΈ°λ°μ΄ λλ°?
νν 무ν μ€ν¬λ‘€μ ꡬνν λ λ κ°μ§ λ°©λ²μ μ¬μ©ν©λλ€.
1. μ€νμ κΈ°λ° νμ΄μ§λ€μ΄μ
2. 컀μ κΈ°λ° νμ΄μ§λ€μ΄μ
μ€νμ
κΈ°λ° νμ΄μ§λ€μ΄μ
μ MySQL κΈ°μ€μΌλ‘ offset, limit μ μ¬μ©ν 쿼리λ₯Ό μ΄μ©ν©λλ€.
νμ§λ§ μ΄λ μ±λ₯ μ ν λ¬Έμ κ° μλλ°, λ°λ‘ offset κ°μ΄ ν΄ λ λ¬Έμ κ° λ°μν©λλ€.
select * from item
order by created_at desc
limit 10
offset 100000000;
μμ κ°μ 쿼리μ κ²½μ° offset κ°μ΄ 1μ΅μ΄κΈ° λλ¬Έμ μμ 1μ΅κ°μ λ°μ΄ν°λ₯Ό λͺ¨λ μ½μ λ€μ, λ€μ 10κ°μ λ°μ΄ν°λ₯Ό μ‘°ννμ¬ μλ΅ν©λλ€. μ΄λ λ€λ‘ κ°μλ‘ μ½μ΄μΌ νλ λ°μ΄ν°κ° λ§μμ§λ€λ κ±Έ λ»νκ³ μ μ λλ €μ§ μ λ°μ μμ΅λλ€.
컀μ κΈ°λ° νμ΄μ§λ€μ΄μ μ μ΄λ¬ν λ¬Έμ μ μ ν΄κ²°ν΄μ€λλ€.
π‘ 컀μ κΈ°λ° νμ΄μ§λ€μ΄μ
- Cursor κ°λ μ μ¬μ©ν©λλ€.
- μ¬μ©μμκ² μλ΅ν΄μ€ λ§μ§λ§ λ°μ΄ν°μ μλ³μ κ°μ Cursorλ‘ μ¬μ©ν©λλ€.
μλ₯Ό λ€μ΄λ³΄κ² μ΅λλ€.
# 1 νμ΄μ§
select * from item
order by id asc
limit 10;
# 2 νμ΄μ§
select * from item
where id > 10 # 1 νμ΄μ§ μ‘°ν κ²°κ³Ό cursor κ°μ΄ 10
order by id asc
limit 10;
1 νμ΄μ§μ μμ²μΌλ‘ μ‘°νλ item λ€μ id λ 1 ~ 10 μ λλ€. μ΄ λ λ§μ§λ§ μλ³μμΈ id 10μ΄ cursorκ° λκ³ μ΄λ₯Ό λ€μ νμ΄μ§ μμ² μ μ¬μ©ν©λλ€. μ€νμ κΈ°λ° νμ΄μ§λ€μ΄μ κ³Ό λΉκ΅ν΄λ³΄λ©΄ λ§μ§λ§μΌλ‘ μ½μ λ°μ΄ν° (id 10) μ λ€μ λ°μ΄ν° (id 11) λΆν° 10κ°λ₯Ό μ‘°ννκΈ° λλ¬Έμ λ§€λ² μνλ λ°μ΄ν° κ°μλ§νΌλ§ μ‘°ννλ€λ μ΄μ μ΄ μμ΅λλ€.
π» 컀μ κΈ°λ° λ¬΄νμ€ν¬λ‘€ ꡬν
μ΄μ Spring μΌλ‘ 무νμ€ν¬λ‘€μ ꡬνν΄λ³΄κ² μ΅λλ€.
μ€ν¬λ‘€ νμ΄μ§λ€μ΄μ μ νΈλ¦¬νκ² κ΅¬ννκΈ° μν ν΄λμ€μ λλ€.
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class ScrollPaginationCollection<T> {
private final List<T> itemsWithNextCursor;// νμ¬ μ€ν¬λ‘€μ μμ + λ€μ μ€ν¬λ‘€μ μμ 1κ° (λ€μ μ€ν¬λ‘€μ΄ μλμ§ νμΈμ μν)private final int countPerScroll;
public static <T> ScrollPaginationCollection<T> of(List<T> itemsWithNextCursor, int size) {
return new ScrollPaginationCollection<>(itemsWithNextCursor, size);
}
public boolean isLastScroll() {
return this.itemsWithNextCursor.size() <= countPerScroll;
}
public List<T> getCurrentScrollItems() {
if (isLastScroll()) {
return this.itemsWithNextCursor;
}
return this.itemsWithNextCursor.subList(0, countPerScroll);
}
public T getNextCursor() {
return itemsWithNextCursor.get(countPerScroll - 1);
}
}
List<T> itemsWithNextCursor: νμ¬ μ€ν¬λ‘€μ λ°μ΄ν° + λ€μ μ€ν¬λ‘€μ λ°μ΄ν° 1κ°λ€μ μ€ν¬λ‘€μ΄ μλμ§ νμΈνκΈ° μν΄ λ€μ μ€ν¬λ‘€μ μμ 1κ°λ₯Ό λ ν¬ν¨ν©λλ€.
int countPerScroll: μ€ν¬λ‘€ 1νμ μ‘°νν λ°μ΄ν°μ κ°μμ λλ€.
boolean isLastScroll(): νμ¬ μ€ν¬λ‘€μ΄ λ§μ§λ§ μ€ν¬λ‘€μΈμ§ νμΈνκΈ° μν λ©μλμ λλ€.μΏΌλ¦¬λ‘ λ°μ΄ν°λ₯Ό μ‘°νν κ²°κ³ΌcountPerScrollμ μ«μ μ΄νλ‘ μ‘°νλλ©΄ λ§μ§λ§ μ€ν¬λ‘€μ΄λΌκ³ νλ¨ν©λλ€.
List<T> getCurrentScrollItems(): λ§μ§λ§ μ€ν¬λ‘€μΌ κ²½μ°itemsWithNextCursorλ₯Ό return νκ³ λ§μ§λ§ μ€ν¬λ‘€μ΄ μλ κ²½μ° λ€μ μ€ν¬λ‘€μ λ°μ΄ν° 1κ°λ₯Ό μ μΈνκ³ return ν©λλ€.
T getNextCursor(): νμ¬ μ€ν¬λ‘€μ λ°μ΄ν° μ€ λ§μ§λ§ λ°μ΄ν°λ₯Ό cursorλ‘ μ¬μ©νκ³ μ΄λ₯Ό return ν©λλ€.
μ€μ μλΉμ€ λ‘μ§μμ ScrollPaginationCollection<T> ν΄λμ€λ₯Ό μ¬μ©ν μμμ λλ€.
public GetFeedsResponse getFeeds(String userEmail, Long roomId, int size, Long lastFeedId) {
User user = FeedServiceUtils.findUserByEmail(userRepository, userEmail);
Room room = FeedServiceUtils.findRoomByRoomId(roomRepository, roomId);
PageRequest pageRequest = PageRequest.of(0, size + 1);
Page<Feed> page = feedRepository.findAllByRoomAndIdLessThanOrderByIdDesc(room, lastFeedId, pageRequest);
List<Feed> feeds = page.getContent();
ScrollPaginationCollection<Feed> feedsCursor = ScrollPaginationCollection.of(feeds, size);
GetFeedsResponse response = GetFeedsResponse.of(feedsCursor, FeedImageCollection.of(feeds, feedImageRepository), feedRepository.countAllByRoom(room));
return response;
}
νμ¬ μλΉμ€ λ‘μ§μμ String userEmail, Long roomId, int size, Long lastFeedId λ₯Ό μΈμλ‘ λ°κ³ μλλ° μ¬κΈ°μ int size, Long lastFeedId μ μ§μ€ν΄μΌ ν©λλ€.
int size: μ€ν¬λ‘€ 1νμ μ‘°νν λ°μ΄ν°μ κ°μ
Long lastFeedId: 컀μλ‘ μ¬μ©νλ λ°μ΄ν° μλ³μμ λλ€.id λ΄λ¦Όμ°¨μμΌλ‘ λ°μ΄ν°λ₯Ό μ‘°ννκΈ° λλ¬Έμ λ€μ μ€ν¬λ‘€μlastFeedIdλ³΄λ€ μμ idμ λ°μ΄ν°λ§ νμΈν©λλ€.
λ€μμ Page<T> μΈν°νμ΄μ€, Pageable μΈν°νμ΄μ€, PageRequest ν΄λμ€μ λν μ΄ν΄κ° νμν©λλ€.
Page<T>μΈν°νμ΄μ€λ νμ΄μ§ μ 보λ₯Ό λ΄μ΅λλ€.
PageableμΈν°νμ΄μ€λ νμ΄μ§ μ²λ¦¬μ νμν μ 보λ₯Ό λ΄κ³ μμ΅λλ€.
PageRequestν΄λμ€λPageableμ μ λ³΄κ° λ΄κ²¨ κ°μ²΄ν λ ν΄λμ€μ λλ€.
JpaRepository κ° μμλ μΈν°νμ΄μ€μ νλΌλ―Έν°λ‘ PageRequest λ₯Ό μ λ¬νλ©΄ Page<T> λ₯Ό return ν©λλ€.
λ€μ getFeeds λ©μλλ₯Ό μ΄ν΄λ΄
μλ€.
PageRequest pageRequest = PageRequest.of(0, size + 1):PageRequestκ°μ²΄μofλ©μλλ μΈμλ‘ μ‘°ννpageμ ν νμ΄μ§λΉ μ‘°νν λ°μ΄ν°μ κ°μsizeλ₯Ό λ°μ΅λλ€. 컀μ κΈ°λ° νμ΄μ§λ€μ΄μ μ΄κΈ° λλ¬Έμ νμlastFeedIdμ΄νμidλ‘λ§ μ‘°ννλ―λ‘ μ²«λ²μ§Έ νμ΄μ§μ μ 보λ₯Ό λ°μΌλ©΄ λ©λλ€.sizeμλ λ€μ μ€ν¬λ‘€μ΄ μλμ§ νλ¨νκΈ° μν΄ λ€μ μ€ν¬λ‘€μ μμ 1κ°λ₯Ό ν¬ν¨νsize + 1μ μ λ ₯ν©λλ€.
Page<Feed> page = feedRepository.findAllByRoomAndIdLessThanOrderByIdDesc(room, lastFeedId, pageRequest):JpaRepositoryλ₯Ό μμνfeedRepositoryμ νλΌλ―Έν°λ‘ 컀μλ‘ μ¬μ©νλlastFeedIdμPageRequestλ₯Ό λ΄μμ λ°μ΄ν°λ₯Ό μ‘°νν©λλ€.
List<Feed> feeds = page.getContent():Page<T>κ° μ 곡νλgetContentλ©μλλ‘ μ‘°νν λ°μ΄ν°λ₯Ό κ°μ Έμ΅λλ€.
ν΄λΌμ΄μΈνΈμκ² μ λ¬ν dtoμΈ GetFeedsResponse ν΄λμ€μ λλ€.
@ToString
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class GetFeedsResponse {
private static final long LAST_CURSOR = -1L;
private List<FeedsInfoResponse> contents = new ArrayList<>();
private long totalElements;
private long nextCursor;
private GetFeedsResponse(List<FeedsInfoResponse> contents, long totalElements, long nextCursor) {
this.contents = contents;
this.totalElements = totalElements;
this.nextCursor = nextCursor;
}
public static GetFeedsResponse of(ScrollPaginationCollection<Feed> feedsScroll, FeedImageCollection feedImages, long totalElements) {
if (feedsScroll.isLastScroll()) {
return GetFeedsResponse.newLastScroll(feedsScroll.getCurrentScrollItems(), feedImages, totalElements);
}
return GetFeedsResponse.newScrollHasNext(feedsScroll.getCurrentScrollItems(), feedImages, totalElements, feedsScroll.getNextCursor().getId());
}
private static GetFeedsResponse newLastScroll(List<Feed> feedsScroll, FeedImageCollection feedImages, long totalElements) {
return newScrollHasNext(feedsScroll, feedImages, totalElements, LAST_CURSOR);
}
private static GetFeedsResponse newScrollHasNext(List<Feed> feedsScroll, FeedImageCollection feedImages, long totalElements, long nextCursor) {
return new GetFeedsResponse(getContents(feedsScroll, feedImages), totalElements, nextCursor);
}
private static List<FeedsInfoResponse> getContents(List<Feed> feedsScroll, FeedImageCollection feedImages) {
return feedsScroll.stream()
.map(feed -> FeedsInfoResponse.of(feed, feedImages.getImagesByFeedId(feed.getId())))
.collect(Collectors.toList());
}
}
List<FeedsInfoResponse> contents: ν΄λΌμ΄μΈνΈμκ² μ΅μ’ μ μΌλ‘ μ λ¬λ λ°μ΄ν°λ€μ λλ€.FeedsInfoResponseλ μλΉμ€ λ‘μ§μμ μ‘°ννFeedλ₯Ό κ°κ³΅ν ννμ λλ€.
long totalElements: μ‘°ν κ°λ₯ν λ°μ΄ν°μ μ΄ κ°μμ λλ€.
long nextCursor: λ€μ μ€ν¬λ‘€μμ μ¬μ©ν 컀μμ κ°μ λλ€.
long LAST_CURSOR = -1L: λ€μ μ€ν¬λ‘€μ΄ μ‘΄μ¬νμ§ μμ κ²½μ°nextCursorμ λ£μ΄μ£ΌκΈ° μν κ°μ λλ€.nextCursor = -1LμΌ κ²½μ° ν΄λΉ μ€ν¬λ‘€μ΄ λ§μ§λ§ μ€ν¬λ‘€μμ λ»ν©λλ€.
List<FeedsInfoResponse> getContents(List<Feed> feedsScroll, FeedImageCollection feedImages):contentsλ‘ μ λ¬ν λ°μ΄ν°λ‘ κ°κ³΅νκΈ° μν λ©μλμ λλ€.
GetFeedsResponse newScrollHasNext(List<Feed> feedsScroll, FeedImageCollection feedImages, long totalElements, long nextCursor): λ€μ μ€ν¬λ‘€μ΄ μ‘΄μ¬νλ κ²½μ°nextCursorμ λ€μ 컀μ κ°μ λ΄μμ κ°μ²΄λ₯Ό μμ±νκΈ° μν λ©μλμ λλ€.
GetFeedsResponse newLastScroll(List<Feed> feedsScroll, FeedImageCollection feedImages, long totalElements): λ€μ μ€ν¬λ‘€μ΄ μ‘΄μ¬νμ§ μμ κ²½μ°nextCursorμ1Lμ λ΄μμ κ°μ²΄λ₯Ό μμ±νκΈ° μν λ©μλμ λλ€.
GetFeedsResponse of(ScrollPaginationCollection<Feed> feedsScroll, FeedImageCollection feedImages, long totalElements): μλΉμ€ λ‘μ§μμλ ν΄λΉ λ©μλλ₯Ό μ¬μ©ν΄μ μ‘°νν λ°μ΄ν°λ₯Ό ν΄λΌμ΄μΈνΈμκ² μ λ¬ν λ°μ΄ν°λ‘ κ°κ³΅ν©λλ€.ScrollPaginationCollectionν΄λμ€μisLastScrollλ©μλλ₯Ό μ¬μ©ν΄μ ν΄λΉ μ€ν¬λ‘€μ΄ λ§μ§λ§ μ€ν¬λ‘€μΈμ§ νμΈν©λλ€. μ΄νμ λ§μ§λ§ μ€ν¬λ‘€μΈμ§ μ¬λΆμ λ°λΌnewLastScrollλλnewScrollHasNextλ©μλλ₯Ό νΈμΆν©λλ€.
λ§μ§λ§μΌλ‘ getFeeds λ©μλλ‘ λμκ°μ λ§λ¬΄λ¦¬ ν΄λ³΄κ² μ΅λλ€.
ScrollPaginationCollection<Feed> feedsCursor = ScrollPaginationCollection.of(feeds, size): μμμ μκ°νScrollPaginationCollection<T>ν΄λμ€μofλ©μλμ μΈμλ‘ScrollPaginationCollectionκ°μ²΄λ₯Ό μμ±ν©λλ€.
GetFeedsResponse response = GetFeedsResponse.of(feedsCursor, FeedImageCollection.of(feeds, feedImageRepository), feedRepository.countAllByRoom(room)): ν΄λΌμ΄μΈνΈμΈ‘μ μ λ¬ν Response νμμΌλ‘ λ³νν΄μ€ λ€ μ΄λ₯Ό return ν©λλ€.
βοΈ μ€μ Response νμΈ
μ€ν¬λ‘€ νμ΄μ§λ€μ΄μ μ΅μ΄ μμ²μ cursor κ°μΌλ‘λ long μ μ΅λκ°μΈ 9223372036854775807 λ₯Ό λ΄μμ μμ²ν©λλ€.
GET localhost:8080/v1/feed?roomId=1&size=1&lastFeedId=9223372036854775807
{
"status": 200,
"message": "OK",
"data": {
"contents": [
{
"createdAt": 1662647379,
"updatedAt": 1662647379,
"feedId": 20,
"userId": 1,
"title": "title",
"content": "content",
"imageUrls": [
"image.png"
]
}
],
"totalElements": 20,
"nextCursor": 20
}
}
κ·Έλ¬λ©΄ μμ κ°μ΄ data μ GetFeedsResponse ννλ‘ κ°κ³΅λ λ°μ΄ν°λ₯Ό νμΈν μ μμ΅λλ€.
λ€μ μμ²μΌλ‘λ lastFeedId μ nextCursor κ°μΈ 20 μ λ΄μμ μμ²ν©λλ€.
GET localhost:8080/v1/feed?roomId=1&size=1&lastFeedId=20
{
"status": 200,
"message": "OK",
"data": {
"contents": [
{
"createdAt": 1662647378,
"updatedAt": 1662647378,
"feedId": 19,
"userId": 1,
"title": "title",
"content": "content",
"imageUrls": [
"image.png"
]
}
],
"totalElements": 20,
"nextCursor": 19
}
}
cursor κ°μΌλ‘ μ
λ ₯νλ 20λ³΄λ€ μμ id μ€ 1κ°λ₯Ό μ‘°ννκΈ° λλ¬Έμ feedId κ° 19 μΈ λ°μ΄ν°κ° μ‘°νλ λͺ¨μ΅μ νμΈν μ μμ΅λλ€.
λ§μ§λ§ μμκ° id = 1 μ΄κΈ° λλ¬Έμ lastFeedId μ 2λ₯Ό λ΄μμ μμ²μ 보λ΄λ³΄κ² μ΅λλ€.
GET localhost:8080/v1/feed?roomId=1&size=1&lastFeedId=2
{
"status": 200,
"message": "OK",
"data": {
"contents": [
{
"createdAt": 1662647366,
"updatedAt": 1662647366,
"feedId": 1,
"userId": 1,
"title": "title",
"content": "content",
"imageUrls": [
"image.png"
]
}
],
"totalElements": 20,
"nextCursor": -1
}
}
λ μ΄μ μ‘°νν λ°μ΄ν°κ° λ¨μ§ μμκΈ° λλ¬Έμ λ€μκ³Ό κ°μ΄ nextCursor μ -1 μ΄ λ΄κΈ΄ λͺ¨μ΅μ νμΈν μ μμ΅λλ€.
βοΈ μ£Όμμ¬ν
μμμ μκ°ν λ°©λ²μ 컀μλ‘ λ°μ΄ν°μ id κ°μ μ¬μ©νμ΅λλ€. MySQL κΈ°μ€μΌλ‘ id μ auto increment μ΅μ
μ μ£Όλ©΄ λ°μ΄ν°κ° μμ±λ λλ§λ€ id κ°μ΄ 1μ© μ¦κ°νκΈ° λλ¬Έμ μμ κ°μ λ°©λ²μΌλ‘ λ°μ΄ν°λ₯Ό μ‘°ννλ©΄ λ°μ΄ν°λ μ΅μ μμΌλ‘ μ‘°νλ©λλ€.
νμ§λ§ λ€λ₯Έ 쑰건μΌλ‘ λ°μ΄ν°λ₯Ό μ λ ¬ν΄μ 무νμ€ν¬λ‘€λ‘ μ‘°ννλ€λ©΄ μ΄λ»κ² λ κΉ?
μλμ κ°μ ν μ΄λΈμ΄ μλ€κ³ κ°μ ν΄λ³΄κ² μ΅λλ€.
| id | index |
| 1 | 2 |
| 2 | 3 |
| 3 | 4 |
| 4 | 1 |
id κ° μλ index κΈ°μ€ λ΄λ¦Όμ°¨μμΌλ‘ μ λ ¬νλ©΄ λ€μκ³Ό κ°μ μμκ° λ©λλ€.
| id | index |
| 3 | 4 |
| 2 | 3 |
| 1 | 2 |
| 4 | 1 |
μ΅μ΄ 컀μ κ°μΌλ‘ lastFeedId = 9223372036854775807 λ₯Ό λ΄μμ μμ²μ 보λ΄κ² λλ©΄ 첫λ²μ§Έ μμμΈ id 3 μ΄ μλλΌ id 4 κ° μ‘°νλ©λλ€. κ·Έλ κΈ° λλ¬Έμ 컀μ κΈ°λ° νμ΄μ§λ€μ΄μ
μ νμ©νλ €λ©΄ 쑰건μ λ§λ 컀μ μ μ μ΄ μ€μν©λλ€.
컀μ μ μ μ΄ μ΄λ ΅λ€λ©΄ μ±λ₯μ 컀μ κΈ°λ° νμ΄μ§λ€μ΄μ λ³΄λ€ λ¨μ΄μ§μ§λ§ μμμ μΈκΈνλ μ€νμ κΈ°λ° νμ΄μ§λ€μ΄μ μ νμ©νλ©΄ μ½κ² ꡬνν μ μμ΅λλ€.
λ€μ κΈμμλ μ€νμ κΈ°λ° νμ΄μ§λ€μ΄μ μ νμ©νλ λ°©λ²μ λν΄ λ€λ€λ³΄κ² μ΅λλ€.