2024. 2. 7. 22:13ㆍ개발일지
페이지네이션을 적용해보기로 했다.
사실 리액트로 간단히 프젝 경험을 했었을 때 맛보기로 페이지네이션을 해보긴 했었는데,,
그때도 어려웠어서 하기 직전에 어쩌지 어쩌지 라고 외치곤 했다
그럼에도 불구하고 어쩌냐고?
뭐 어쩌겠어 해야지 ㅎ하하하ㅏ
먼저, 편지를 전체 조회할때 리스트로 받았던 것을 pageable를 이용해서 간편하게 구현해보고자 하였다.
Pageable는 Spring에서는 Pagination을 지원하는 인터페이스를 말한다.
간단하게 설명을 하자면,
getPageNumber() : 현재 페이지 번호를 반환(0부터 시작)
getPageSize() : 한 페이지당 최대 항목 수를 반환
getOffset() : 현재 페이지의 시작 위치를 반환
getSort() : 정렬 정보를 반환
next() : 다음 페이지 정보를 반환
previous() : 이전 페이지 정보를 반환
Pageable 을 이용해서 페이지 번호, 페이지당 항목 수, 필요에 따라 정렬 정보를 추가로 지정할 수 있다.
Pageable 로 지정한 정보들을 가지고 Page 객체를 반환할 수 있고, Page 객체는 조회된 데이터와 페이지 정보를 함께 갖게 된다.
page : 조회할 페이지 번호(0부터 시작)
size : 한 페이지당 최대 항목 수
sort : 정렬 정보(생략 가능)
direction : 정렬 방향(ASC, DESC)
properties : 정렬 대상 속성명
을 활용하여 페이지네이션을 구현할 수 있다.
그래서 나는 편지를 전체 조회하는 부분에서
fun getListLetter(
@PageableDefault(size = 5, sort = ["createdAt"], direction = Sort.Direction.DESC) pageable: Pageable
): ResponseEntity<Page<LetterResponse>> {
return ResponseEntity
.status(HttpStatus.OK)
.body(adminService.getLetterList(pageable))
컨트롤러 부분을 이런식으로 작성해주었다. createdAt 기준으로 정렬을 해주었다.
override fun getLetterList(pageable: Pageable): Page<LetterResponse> {
return letterRepository.findAll(pageable).map { it.toResponse() }
}
서비스쪽도 마찬가지로 작성해주었다. 이런식으로 간단하게 pageable를 받아서 할 수 있었고
비교적 페이지네이션을 손쉽게 작성을 할 수 있었다.
그러나, 나는 여기서 만족하지 않고 다른 방법의 페이지네이션도 적용해보려고 했다.
그저 pageable를 사용하는 것이 아니라 다른 방법도 있지 않을까라고 생각했기 때문이다.
그래서 이처럼 requestParam으로 각각 페이지 넘버, 사이즈, 정렬하는 것, 내림차순? 을 받고 작성을 해주었다.
@Operation(summary = "user 전체 페이지 네이션 조회 ONLY ADMIN")
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
fun getUserPage(
@RequestParam(defaultValue = "1") pageNumber: Int,
@RequestParam(defaultValue = "5") pageSize: Int,
@RequestParam(defaultValue = "createdAt") sort: String?,
@RequestParam direction: Sort.Direction
): ResponseEntity<UserPageResponse> {
return ResponseEntity.status(HttpStatus.OK)
.body(adminService.getUserPage(pageNumber, pageSize, sort, direction))
}
이렇게 다른식으로 조금씩 변형을 하여 작성해주는 방법도 있었다.
override fun getUserPage(
pageNumber: Int,
pageSize: Int,
sort: String?,
direction: Sort.Direction
): UserPageResponse {
val pageable: PageRequest = PageRequest.of(pageNumber - 1, pageSize, direction, sort)
val usersPage: Page<User> = userRepository.findAll(pageable)
val usersResponseList: List<UserResponse> = usersPage.content.map { it.toResponse() }
return UserPageResponse(
content = usersResponseList,
totalPages = usersPage.totalPages,
totalUsers = usersPage.totalElements
)
}
PageRequest는 Spring Data JPA에서 제공하는 Pageable 구현체 중 하나로, 페이지 정보를 생성하는 클래스라고 한다. 그래서 이를 이용해서 서비스도 적용해주었다. 어려웠지만 공부하는 거겠거니 하고 참고해서 작성해주었다..
마지막으로 레포지토리 impl을 만들어서 작성해주었다.
앞서 레포지토리 impl를 만든 이유에 대해서 블로그를 작성했던 적이 있는데 이번에는 직접 적용해서 페이지네이션을 적용해보려고 한다.
private val user = QUser.user
override fun findByPageableUserAndStatus(pageable: Pageable): Page<User> {
val whereClause = BooleanBuilder()
val totalCount = queryFactory.select(user.count()).from(user).where(whereClause).fetchOne() ?: 0L
val query = queryFactory.selectFrom(user)
.where(whereClause)
.offset(pageable.offset)
.limit(pageable.pageSize.toLong())
if (pageable.sort.isSorted) {
when (pageable.sort.first()?.property) {
"id" -> query.orderBy(user.id.asc())
"email" -> query.orderBy(user.email.asc())
else -> query.orderBy(user.id.asc())
}
} else {
query.orderBy(user.id.asc())
}
val contents = query.fetch()
return PageImpl(contents, pageable, totalCount)
}
pageable를 받는 것은 그대로 하고 정적쿼리를 하여 직접 페이지네이션을 작성해주었다.
offset으로 해주었으며 정렬순은 id 오름차순, email 오름차순 식으로 작성해주었다.
사실 이걸 정확히 완벽히 이해하고 썼던건 아니지만 시도를 한번이라도 할 수 있었고 추후엔 이를 이용해서 정적쿼리를 자유자재로 쓰도록 공부할 예정이다. 파이팅,, ㅎㅎ
이렇게 다양한 방법으로 페이지네이션을 하는 것을 배울 수 있었고 적용하였다..
좀 더 다른 방법이 있다면 알아보고 추가로 적용해봐야겠다 ㅎㅎ
논리적 삭제
튜터님께서 어느 날 말씀해주셨다.
실제 현업에서는 그저 삭제를 하는 것이 아니라 삭제를 boolean으로 받아서 데이터를 보관하는 논리적 삭제를 대부분 한다고 말이다. 음 이게 처음엔 이해하지 못했으나 생각해보니 중요한 정보를 그냥 클릭 한번으로 사라질 수 있다는 것은 위험부담이 되지 않겠는가 라고 깨달았다. 그래서 많은 회사에서도 논리적 삭제로 삭제를 구현하고 있구나 라고 또 하나 배울 수 있었다.
이를 두면 조회할때는 삭제된걸로 취급? 당하기 때문에 보여지지 않는 장점을 가지고 있다.
우선 엔티티에 isdelete라는 컬럼을 새로 추가하고 boolean 값으로 받는다.
@Column(name = "is_deleted")
var isDeleted: Boolean = false
그리고 위에
@SQLRestriction("is_deleted = false")
이런 어노테이션을 추가로 해주었다.
어? 정말 조회할때 true 값은 조회가 되지 않았다.
서비스 쪽에 삭제 하게 되면 true 값으로 변하게 했기 때문이다.
fun deleteLetter() {
isDeleted = true
}
근데 여기서 잠깐 저기 두번째에 제약을 걸어둔게 있어서 문제가 발생했다.
보통 논리적 삭제라고 해도 데이터를 주기적으로 비어줘야 과부하가 발생하지 않는다.
그래서 주기적으로 삭제로직을 돌릴 수 있게 스케줄러를 이용해서 구현을 하였는데,
이상하게 이는 삭제되지 않는데 다른 건 삭제가 되는 상황이 발생하였다.
이 차이가 이상하여서 코드를 꼼꼼히 살펴본 결과
@SQLRestriction("is_deleted = false")
이에 대한 유무였다.. 이가 있으면 삭제로직도 발동을 안하는 것이였다 ㅎ하ㅏ
이를 지우니 정상적으로 작동되었다...
ㅎㅎ 그래도 코드를 꼼꼼히 본 보람이 있었다나,, ㅎ하
그리고 나는 저렇게 서비스 쪽에서 항상 true 값으로 변하는 로직을 줬었는데 그 방법도 있지만
어노테이션으로 엔티티에 직접 쿼리를 작성해주는 방법도 있다고 한다!
이것도 역시 작업을 해볼 예정이다
뭐든 어떤 거든 다양한 시도를 해보는 것이 중요하다고 생각해서,,
일단 오늘 페이지 네이션이랑 논리적 삭제에 대해서 정리하는 시간을 가졌는데,,
이만큼했다 해서 만족하지말고 다양한 방법들을 고안해서 더 알맞는 방법을 픽해서 정해야겠다 싶다
파이팅 ! 고생했다 오늘도
'개발일지' 카테고리의 다른 글
TIL 20240213 - map과 forEach는 어떤 것이 다를까? (0) | 2024.02.13 |
---|---|
WIL 20240210 (2) | 2024.02.10 |
WIL 20240203 (0) | 2024.02.03 |
WIL 20240103 (0) | 2024.01.03 |
SQL 코드카다 푼 거 중 기억해야 할 것 메모해두기 (0) | 2023.12.25 |