본문 바로가기
Study

Pagination - 페이지네이션

by Jiwon_Loopy 2025. 7. 27.
반응형

오프셋 있는 버전

https://youtu.be/r6TddguJIQo

 

오프셋 없는 버전

https://youtu.be/4ALgfOBnkvM

 

 

📊 목차

  1. 페이징 시스템 개요
  2. PageDTO 클래스 분석
  3. PageMaker 클래스 분석
  4. 실제 사용 사례
  5. 아키텍처 및 데이터 흐름
  6. 장점 및 특징
  7. 성능 최적화
  8. 확장성 및 유지보수성

🎯 페이징 시스템 개요

왜 페이징이 필요한가?

  1. 성능 최적화
    • 대용량 데이터 로딩 시 메모리 사용량 제한
    • 데이터베이스 쿼리 성능 향상
    • 네트워크 트래픽 감소
  2. 사용자 경험 개선
    • 빠른 페이지 로딩 속도
    • 직관적인 네비게이션
    • 모바일 환경 최적화
  3. 서버 리소스 효율성
    • 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

설계 철학

  1. 단순성: 복잡한 로직 없이 핵심 기능만 제공
  2. 안전성: 잘못된 입력값에 대한 방어 로직
  3. 표준화: 모든 페이징 요청에 일관된 인터페이스
  4. 확장성: 상속을 통한 기능 확장 가능

🏗️ 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}

데이터 흐름 상세

  1. 클라이언트 요청
  2. fetch('/api/stores?offset=20&pageSize=10&category=한식')
  3. Controller 처리
  4. PageDTO pageDTO = new PageDTO(); pageDTO.setCurrentPage(3); // offset 20 → 페이지 3 pageDTO.setPageSize(10);
  5. Service 처리
  6. PageMaker<StoreVO> pageMaker = new PageMaker<>( storeList, // 10개의 가게 데이터 totalCount, // 전체 150개 10, // 페이지당 10개 3 // 현재 3페이지 );
  7. 응답 생성
  8. { "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;
    }
}


🎯 결론

구현 성과

  1. 표준화된 페이징 시스템: 모든 목록 조회에 일관된 인터페이스 제공
  2. 성능 최적화: 데이터베이스 쿼리 및 메모리 사용량 최적화
  3. 사용자 경험 향상: 빠른 로딩 속도와 직관적인 네비게이션
  4. 확장성: 상속을 통한 기능 확장 및 새로운 요구사항 대응

기술적 가치

  • 제네릭 활용: 타입 안전성과 재사용성 확보
  • 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