반응형
오프셋 있는 버전
오프셋 없는 버전
📊 목차
🎯 페이징 시스템 개요
왜 페이징이 필요한가?
- 성능 최적화
- 대용량 데이터 로딩 시 메모리 사용량 제한
- 데이터베이스 쿼리 성능 향상
- 네트워크 트래픽 감소
- 사용자 경험 개선
- 빠른 페이지 로딩 속도
- 직관적인 네비게이션
- 모바일 환경 최적화
- 서버 리소스 효율성
- CPU 및 메모리 사용량 최적화
- 동시 접속자 처리 능력 향상
Sol-Food의 페이징 전략
- 표준화된 페이징 시스템: 모든 목록 조회에 일관된 페이징 적용
- 유연한 페이지 크기: 상황에 따른 동적 페이지 크기 조정
- 검색과 페이징 통합: 검색 결과에도 페이징 적용
- 실시간 데이터 처리: AJAX 기반 무한 스크롤 지원
🔧 PageDTO 클래스 분석
클래스 구조
@Data
public class PageDTO {
private final int MAX_PAGE_SIZE = 50; // 최대 페이지 크기
private final int DEFAULT_PAGE_SIZE = 10; // 기본 페이지 크기
private int currentPage = 1; // 현재 페이지
int pageSize = 10; // 페이지 크기
int offset = 0; // 오프셋 (계산값)
}
핵심 메서드
1. setCurrentPage(int currentPage)
public void setCurrentPage(int currentPage) {
this.currentPage = Math.max(currentPage, 1); // 최소값 1 보장
}
- 기능: 현재 페이지 설정
- 안전장치: 음수나 0 입력 시 1로 자동 조정
- 목적: 잘못된 페이지 번호 입력 방지
2. setPageSize(int pageSize)
public void setPageSize(int pageSize) {
this.pageSize = pageSize > MAX_PAGE_SIZE || pageSize <= DEFAULT_PAGE_SIZE
? DEFAULT_PAGE_SIZE : pageSize;
}
- 기능: 페이지 크기 설정
- 제한사항:
- 최대 50개 (MAX_PAGE_SIZE)
- 최소 10개 (DEFAULT_PAGE_SIZE)
- 목적: 서버 부하 방지 및 일관된 UX
3. getOffset()
public int getOffset() {
return (currentPage - 1) * pageSize; // SQL LIMIT OFFSET 계산
}
- 기능: SQL 쿼리의 OFFSET 값 계산
- 공식: (현재페이지 - 1) × 페이지크기
- 예시:
- 1페이지, 10개씩 → offset = 0
- 2페이지, 10개씩 → offset = 10
- 3페이지, 10개씩 → offset = 20
설계 철학
- 단순성: 복잡한 로직 없이 핵심 기능만 제공
- 안전성: 잘못된 입력값에 대한 방어 로직
- 표준화: 모든 페이징 요청에 일관된 인터페이스
- 확장성: 상속을 통한 기능 확장 가능
🏗️ PageMaker 클래스 분석
클래스 구조
@Data
public class PageMaker<T> {
private List<T> list; // 현재 페이지 데이터
private Integer pageCount; // 전체 페이지 수
private Integer limit; // 페이지당 데이터 수
private int curPage; // 현재 페이지
private long count; // 전체 데이터 수
private int totalPageCount; // 전체 페이지 수 (중복)
private int firstPage; // 현재 그룹의 첫 페이지
private int lastPage; // 현재 그룹의 마지막 페이지
}
생성자 분석
public PageMaker(List<T> list, long count, Integer limit, int curPage) {
this.list = list; // 페이지에 해당 데이터
this.count = count; // 전체 데이터 수
this.limit = limit; // 페이지당 데이터 수
this.curPage = curPage; // 현재 페이지
final int pageGroupCount = 10; // 한 그룹당 페이지 수
if (limit != null) {
// 전체 페이지 수 계산
this.pageCount = (int) Math.ceil((double) count / limit);
// 페이지 그룹 수 계산
final int pageGroup = (int) Math.ceil((double) curPage / pageGroupCount);
// 현재 페이지 그룹의 시작/끝 페이지 계산
this.firstPage = ((pageGroup - 1) * limit) + 1;
this.lastPage = Math.min(pageGroup * limit, pageCount);
}
}
핵심 계산 로직
1. 전체 페이지 수 계산
this.pageCount = (int) Math.ceil((double) count / limit);
- 공식: 전체 데이터 수 ÷ 페이지당 데이터 수 (올림)
- 예시:
- 전체 95개, 페이지당 10개 → 10페이지
- 전체 100개, 페이지당 10개 → 10페이지
- 전체 101개, 페이지당 10개 → 11페이지
2. 페이지 그룹 계산
final int pageGroup = (int) Math.ceil((double) curPage / pageGroupCount);
- 목적: 페이지네이션 UI에서 그룹별 표시
- 예시:
- 현재 15페이지, 그룹당 10개 → 2번째 그룹
- 현재 25페이지, 그룹당 10개 → 3번째 그룹
3. 그룹 범위 계산
this.firstPage = ((pageGroup - 1) * limit) + 1;
this.lastPage = Math.min(pageGroup * limit, pageCount);
- firstPage: 현재 그룹의 첫 페이지 번호
- lastPage: 현재 그룹의 마지막 페이지 번호
- 예시:
- 2번째 그룹, 페이지당 10개 → 11~20페이지
- 마지막 그룹이 10개 미만일 경우 실제 페이지 수로 제한
제네릭 활용
public class PageMaker<T>
- 장점: 모든 데이터 타입에 대해 재사용 가능
- 사용 예시:
- PageMaker<StoreVO>: 가게 목록 페이징
- PageMaker<UserVO>: 사용자 목록 페이징
- PageMaker<BoardVO>: 게시글 목록 페이징
💼 실제 사용 사례
1. 가게 목록 페이징 (StoreController)
@GetMapping("/api/list")
@ResponseBody
public StoreListResponseVO getStoreListAjax(
@RequestParam(value = "category", required = false) String category,
@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "pageSize", defaultValue = "10") int pageSize,
@RequestParam(value = "sort", defaultValue = "name") String sort,
HttpSession session) {
// PageDTO 설정
PageDTO pageDTO = new PageDTO();
pageDTO.setCurrentPage(offset / pageSize + 1); // offset을 페이지 번호로 변환
pageDTO.setPageSize(pageSize);
// 페이징된 데이터 조회
PageMaker<StoreVO> pageMaker = service.getPagedCategoryStoreListWithLike(
searchCategory, pageDTO, loginUser.getUsersId(), sort);
// 다음 페이지 존재 여부 확인
boolean hasNext = offset + pageSize < pageMaker.getCount();
return StoreListResponseVO.success(
pageMaker.getList(),
hasNext,
offset,
pageSize,
pageMaker.getCount()
);
}
2. 게시글 목록 페이징 (BoardController)
@GetMapping("/api/list")
@ResponseBody
public BoardListResponseVO list(
@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "pageSize", defaultValue = "10") int pageSize) {
// PageDTO 설정
PageDTO pageDTO = new PageDTO();
pageDTO.setCurrentPage(offset / pageSize + 1);
pageDTO.setPageSize(pageSize);
// 페이징된 게시글 조회
PageMaker<BoardVO> pageMaker = boardService.getBoardList(pageDTO);
boolean hasNext = offset + pageSize <= pageMaker.getCount();
return BoardListResponseVO.success(
pageMaker.getList(),
hasNext,
offset,
pageSize,
pageMaker.getCount()
);
}
3. 관리자 사용자 관리 페이징
// UserSearchRequestDTO (PageDTO 상속)
@Data
public class UserSearchRequestDTO extends PageDTO {
private String query; // 검색어
}
// AdminHomeServiceImpl
@Override
public PageMaker<UserSearchResponseDTO> getUsers(UserSearchRequestDTO request) {
List<UserSearchResponseDTO> users = adminMapper.getUsers(request);
int totalCount = adminMapper.getUsersCount(request);
return new PageMaker<>(
users,
totalCount,
request.getPageSize(),
request.getCurrentPage()
);
}
4. 검색과 페이징 통합
// 검색 결과 페이징
PageMaker<StoreVO> getPagedSearchResults(String keyword, PageDTO pageDTO) {
List<StoreVO> list = mapper.selectPagedSearchResults(
keyword,
pageDTO.getOffset(),
pageDTO.getPageSize()
);
long total = mapper.countSearchResults(keyword);
return new PageMaker<>(list, total, pageDTO.getPageSize(), pageDTO.getCurrentPage());
}
🔄 아키텍처 및 데이터 흐름
전체 시스템 구조
클라이언트 요청 (offset, pageSize)
↓
Controller (PageDTO 생성)
↓
Service (PageMaker 생성)
↓
Mapper (SQL 쿼리 실행)
↓
데이터베이스 (LIMIT OFFSET)
↓
PageMaker (페이징 정보 계산)
↓
ResponseVO (클라이언트 응답)
SQL 쿼리 예시
-- 전체 데이터 수 조회
SELECT COUNT(*) FROM stores WHERE category = #{category}
-- 페이징된 데이터 조회
SELECT * FROM stores
WHERE category = #{category}
ORDER BY store_name ASC
LIMIT #{pageSize} OFFSET #{offset}
데이터 흐름 상세
- 클라이언트 요청
- fetch('/api/stores?offset=20&pageSize=10&category=한식')
- Controller 처리
- PageDTO pageDTO = new PageDTO(); pageDTO.setCurrentPage(3); // offset 20 → 페이지 3 pageDTO.setPageSize(10);
- Service 처리
- PageMaker<StoreVO> pageMaker = new PageMaker<>( storeList, // 10개의 가게 데이터 totalCount, // 전체 150개 10, // 페이지당 10개 3 // 현재 3페이지 );
- 응답 생성
- { "list": [...], // 10개 가게 데이터 "hasNext": true, // 다음 페이지 존재 "offset": 20, // 현재 오프셋 "pageSize": 10, // 페이지 크기 "totalCount": 150 // 전체 데이터 수 }
⭐ 장점 및 특징
1. 표준화된 인터페이스
// 모든 페이징 요청이 동일한 구조
public class UserSearchRequestDTO extends PageDTO {
private String query;
}
public class StoreSearchRequestDTO extends PageDTO {
private String category;
private String sort;
}
2. 타입 안전성
// 제네릭을 통한 타입 안전성
PageMaker<StoreVO> storePageMaker;
PageMaker<UserVO> userPageMaker;
PageMaker<BoardVO> boardPageMaker;
3. 유연한 페이지 크기
// 상황에 따른 동적 페이지 크기
pageDTO.setPageSize(5); // 모바일: 작은 페이지
pageDTO.setPageSize(10); // 데스크톱: 기본 크기
pageDTO.setPageSize(20); // 관리자: 큰 페이지
4. 검색과 페이징 통합
// 검색 조건과 페이징 정보를 함께 처리
public class PaymentSearchRequestDto extends PageDTO {
private LocalDateTime fromDate;
private LocalDateTime toDate;
private String paymentMethod;
private String query;
}
5. 무한 스크롤 지원
// 클라이언트 사이드 무한 스크롤
function loadMoreStores() {
fetch(`/api/stores?offset=${currentOffset}&pageSize=10`)
.then(response => response.json())
.then(data => {
if (data.hasNext) {
appendStores(data.list);
currentOffset += data.pageSize;
}
});
}
🚀 성능 최적화
1. 데이터베이스 최적화
-- 인덱스 활용
CREATE INDEX idx_store_category ON stores(category);
CREATE INDEX idx_store_name ON stores(store_name);
-- 복합 인덱스
CREATE INDEX idx_store_category_name ON stores(category, store_name);
2. 쿼리 최적화
// COUNT 쿼리 최적화
@Select("SELECT COUNT(*) FROM stores WHERE category = #{category}")
long countStoresByCategory(String category);
// 페이징 쿼리 최적화
@Select("SELECT * FROM stores WHERE category = #{category} " +
"ORDER BY store_name ASC LIMIT #{pageSize} OFFSET #{offset}")
List<StoreVO> selectPagedCategoryStores(@Param("category") String category,
@Param("offset") int offset,
@Param("pageSize") int pageSize);
3. 메모리 최적화
// 페이지 크기 제한으로 메모리 사용량 제어
private final int MAX_PAGE_SIZE = 50; // 최대 50개씩만 로드
// 불필요한 데이터 로딩 방지
public void setPageSize(int pageSize) {
this.pageSize = pageSize > MAX_PAGE_SIZE ? DEFAULT_PAGE_SIZE : pageSize;
}
4. 네트워크 최적화
// 필요한 데이터만 전송
public class StoreListResponseVO {
private List<StoreVO> list; // 실제 데이터
private boolean hasNext; // 다음 페이지 여부
private long totalCount; // 전체 개수
// 불필요한 정보는 제외
}
🔧 확장성 및 유지보수성
1. 상속을 통한 확장
// 기본 PageDTO 확장
public class UserSearchRequestDTO extends PageDTO {
private String query;
private String status;
private Date fromDate;
private Date toDate;
}
public class PaymentSearchRequestDto extends PageDTO {
private LocalDateTime fromDate;
private LocalDateTime toDate;
private String paymentMethod;
private String paymentStatus;
}
2. 새로운 페이징 요구사항 대응
// 정렬 기능 추가
public class SortablePageDTO extends PageDTO {
private String sortBy = "id";
private String sortOrder = "ASC";
public String getOrderByClause() {
return sortBy + " " + sortOrder;
}
}
// 필터링 기능 추가
public class FilterablePageDTO extends PageDTO {
private Map<String, Object> filters = new HashMap<>();
public void addFilter(String key, Object value) {
filters.put(key, value);
}
}
3. 테스트 용이성
@Test
public void testPageDTO() {
PageDTO pageDTO = new PageDTO();
pageDTO.setCurrentPage(2);
pageDTO.setPageSize(10);
assertEquals(10, pageDTO.getOffset()); // (2-1) * 10 = 10
assertEquals(10, pageDTO.getPageSize());
}
@Test
public void testPageMaker() {
List<StoreVO> stores = Arrays.asList(store1, store2, store3);
PageMaker<StoreVO> pageMaker = new PageMaker<>(stores, 25, 10, 2);
assertEquals(3, pageMaker.getPageCount()); // 25개 ÷ 10개 = 3페이지
assertEquals(2, pageMaker.getCurPage());
}
4. 문서화 및 표준화
/**
* 페이징 정보를 담는 DTO 클래스
*
* @author Sol-Food Team
* @version 1.0
* @since 2024-01-01
*/
@Data
public class PageDTO {
/** 최대 페이지 크기 */
private final int MAX_PAGE_SIZE = 50;
/** 기본 페이지 크기 */
private final int DEFAULT_PAGE_SIZE = 10;
/** 현재 페이지 (1부터 시작) */
private int currentPage = 1;
/** 페이지당 데이터 수 */
private int pageSize = 10;
/**
* SQL OFFSET 값을 계산하여 반환
* @return 계산된 OFFSET 값
*/
public int getOffset() {
return (currentPage - 1) * pageSize;
}
}
🎯 결론
구현 성과
- 표준화된 페이징 시스템: 모든 목록 조회에 일관된 인터페이스 제공
- 성능 최적화: 데이터베이스 쿼리 및 메모리 사용량 최적화
- 사용자 경험 향상: 빠른 로딩 속도와 직관적인 네비게이션
- 확장성: 상속을 통한 기능 확장 및 새로운 요구사항 대응
기술적 가치
- 제네릭 활용: 타입 안전성과 재사용성 확보
- SOLID 원칙: 단일 책임 원칙과 개방-폐쇄 원칙 준수
- 테스트 용이성: 단위 테스트 작성 가능한 구조
- 문서화: 명확한 주석과 문서화
비즈니스 가치
- 개발 효율성: 표준화된 페이징으로 개발 시간 단축
- 유지보수성: 일관된 구조로 유지보수 용이
- 확장성: 새로운 기능 추가 시 기존 코드 영향 최소화
- 성능: 대용량 데이터 처리 시 안정적인 성능 보장
728x90
반응형
'Study' 카테고리의 다른 글
| Google Analytics - 구글 애널리틱스 (3) | 2025.07.27 |
|---|---|
| 오목 구현해보기 (0) | 2025.04.20 |
| 정보 처리 기사 실기 용어 정리 (0) | 2025.04.16 |
| 3월 4째주 기록 (0) | 2025.03.28 |
| 3월 3째주 기록 (0) | 2025.03.17 |