Be My Story에는 친구들과 다른 유저의 컨텐츠를 모아 볼 수 있는 타임라인이 존재한다.
Be My Story는 SNS이기 때문에 무한 스크롤이 어울리겠다고 생각했다.
큰 Result Set을 JPA로 처리하는 방법은 크게 Slice, Page, Stream 3가지로 나누어볼 수 있다.
Slice와 Page가 paginated query를 사용하는 방식으로 모든 entity들을 작은 batch로 나누어,
메모리에 많은 데이터를 로드하는 것을 막는다.
나는 Slice를 이용해서 무한 페이지네이션을 구현해보았다.
Entity
Book entity를 data로 사용한다.
@Entity
public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "book_id")
private Long bookId;
@Column(name = "title")
private String title;
@Column(name = "title_x")
private int titleX;
@Column(name = "title_y")
private int titleY;
@Column(name = "genre")
private String genre;
@Column(name = "date")
private String date;
@JsonIgnore
@OneToOne
@JoinColumn(name = "diary_id")
private Diary diary;
@JsonIgnore
@OneToMany(mappedBy = "book", fetch = FetchType.LAZY)
private List<Text> texts;
@JsonIgnore
@OneToMany(mappedBy = "book", fetch = FetchType.LAZY)
private List<Image> images = new ArrayList<>();
@JsonIgnore
@OneToOne(mappedBy = "book", fetch = FetchType.LAZY)
private Cover cover;
@JsonIgnore
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@CreationTimestamp
@Column(name = "created_at")
private Timestamp createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private Timestamp updatedAt;
@Column(name = "deleted_at")
private Timestamp deletedAt;
@Column(name = "is_deleted", columnDefinition = "boolean default false")
private boolean isDeleted;
}
Controller
@RestController
@RequiredArgsConstructor
public class TimeLineController {
private final TimeLineService timeLineService;
@GetMapping("/timeline")
public ResponseEntity<SliceResponse> timeline(Authentication auth, Pageable pageable){
return ResponseEntity.ok().body(timeLineService.processBookExceptNowUser(auth.getName(), pageable));
}
}
JWT 토큰에 담긴 userName을 통해 현재 로그인한 사용자를 알아내고, 현재 사용자를 제외한 모든 사용자의 동화책을 Slice로 순차적으로 받아온다.
순차적으로 받아오는데 Pageable 인스턴스가 쓰인다.Pageable 인스턴스에는 페이지 번호(page), 한 페이지에 불러올 데이터 건수(size), 정렬 조건(sort) 이 들어있다.
요청 파라미터는 ?page=0&size=4&sort=desc와 같지만, @RequestParam 어노테이션을 쓰지 않고,
그냥 Controller 메서드 파라미터에 Pageable을 넣어주면 된다.
Service
public class TimeLineService {
private final StoryBookRepository bookRepository;
private final UserRepository userRepository;
public SliceResponse processBookExceptNowUser(String userName, Pageable pageable){
User user = userRepository.findByUserName(userName).orElseThrow();
Slice<BookDTO.BookMeta> slice = bookRepository.findAllByUserNotOrderByBookIdDesc
(user, pageable);
return new SliceResponse(slice);
}
}
깔끔한 응답을 위해 SliceResponse 클래스를 만들었다.
SliceResponse
@Getter
public class SliceResponse<T> {
private final List<T> content;
private final SortResponse sort;
private final int currentPage;
private final int size;
private final boolean first;
private final boolean last;
public SliceResponse(Slice<T> sliceContent){
this.content=sliceContent.getContent();
this.sort=new SortResponse(sliceContent.getSort());
this.currentPage=sliceContent.getNumber();
this.size=sliceContent.getSize();
this.first=sliceContent.isFirst();
this.last=sliceContent.isLast();
}
}
Repository
현재 로그인한 사용자를 제외한 모든 사람들의 Book이 타임라인에 뜨길 바랐기 때문에 아래와 같이 메서드를 작성했다.
또한 최신순으로 보여주고 싶어서 BookId의 내림차순으로 데이터를 가져오게 했다.
@Repository
public interface StoryBookRepository extends JpaRepository<Book, Long> {
Slice<BookDTO.BookMeta> findAllByUserNotOrderByBookIdDesc(User user, Pageable page);
}
여기서 주목할 점은 return type이 Slice<BookDTO.BookMeta>라는 것이다.
BookDTO.BookMeta는 Book의 DTO이다. 이번에 JPA에서 바로 DTO 형식으로 return할 수 있다는 것도 배웠다.
Slice object는 parameter로 들어오는 Pageable 객체에 따라 첫 번째 페이지부터 마지막 페이지까지 알아서 리턴한다.
결과
{
"content": [
{
"bookId": 13,
"title": "오늘은 사아",
"bookX": 0,
"bookY": 0,
"coverUrl": "https://000.s3.ap-northeast-2.amazonaws.com/20",
"date": "2023-09-27"
},
{
"bookId": 12,
"title": "오늘은 마바",
"bookX": 0,
"bookY": 0,
"coverUrl": "https://000.s3.ap-northeast-2.amazonaws.com/20c",
"date": "2023-09-27"
},
{
"bookId": 11,
"title": "오늘은 다라",
"bookX": 0,
"bookY": 0,
"coverUrl": "https://000.s3.ap-northeast-2.amazonaws.com/2",
"date": "2023-09-27"
},
{
"bookId": 10,
"title": "오늘은 27일 화요일",
"bookX": 0,
"bookY": 0,
"coverUrl": "https://000.s3.ap-northeast-2.amazonaws.com/",
"date": "2023-09-27"
}
],
"sort": {
"sorted": true,
"direction": "DESC",
"orderProperty": "BookId"
},
"currentPage": 0,
"size": 4,
"first": true,
"last": false
}
content에는 타임라인에 표시될 동화 컨텐츠들이 들어가 있고,
sort에 이 slice에 대한 정보가 들어있다.
참고
Slice 사용법
https://www.baeldung.com/spring-data-jpa-iterate-large-result-sets#overview
JPA에서 Query 사용법
https://sundries-in-myidea.tistory.com/91
'캡스톤프로젝트' 카테고리의 다른 글
스프링부트로 팔로우/팔로잉 기능을 구현해보자 (4) | 2023.11.13 |
---|---|
[HTTP Method] PUT vs PATCH 결정하기 (0) | 2023.09.22 |
[Nginx, Amazon Linux] Certbot을 이용한 https 인증받기 (0) | 2023.09.13 |
졸업프로젝트, 태초마을로 돌아가다 (0) | 2023.09.02 |
[SpringBoot] 팔로워, 팔로잉 기능 구현하기 2 (0) | 2023.08.30 |