중요)서버에서 응답 데이터를 N개씩 뿌려줄 '데이터베이스 페이징'기법

Songss·2024년 12월 13일

DBMS

목록 보기
13/20

데이터베이스 페이징(Paging)

데이터베이스 페이징은 큰 데이터셋을 작은 페이지 단위로 나누어 필요한 만큼만 가져오는 기술입니다. 대량의 데이터를 효율적으로 처리하고 클라이언트와 서버의 리소스 사용을 최적화하기 위해 사용됩니다.


왜 페이징이 필요한가?

  1. 성능 향상: 한 번에 모든 데이터를 클라이언트로 보내면 서버 부하와 네트워크 트래픽이 커집니다.
  2. 사용자 경험 개선: 대량의 데이터를 한 번에 로드하면 페이지가 느려질 수 있습니다. 필요한 만큼만 보여주면 응답 속도가 빨라집니다.
  3. 효율적인 데이터 처리: 대량의 데이터를 한 번에 처리하지 않고 필요한 범위의 데이터만 처리합니다.

페이징의 작동 방식

1. 요청

클라이언트는 페이지 번호와 페이지당 표시할 데이터 수를 요청합니다.

예: GET /api/books?page=2&size=10

  • page=2: 2번째 페이지
  • size=10: 한 페이지에 10개의 데이터 표시

2. 데이터베이스 쿼리

서버는 데이터베이스에서 요청받은 페이지에 해당하는 데이터만 가져옵니다.

SQL의 LIMITOFFSET을 사용하여 원하는 데이터만 쿼리합니다.


SQL로 페이징 구현

LIMIT와 OFFSET 사용

SELECT * FROM books
LIMIT 10 OFFSET 10;
  • LIMIT: 가져올 데이터 개수
  • OFFSET: 건너뛸 데이터 개수 (페이지 번호에 따라 달라짐)

계산 방법

  • OFFSET = (page - 1) × size
    • 예: page=2, size=10이면 OFFSET은 (2-1) × 10 = 10

Express와 페이징 구현 예제

const getPagedBooks = (req, res) => {
  let { page = 1, size = 10 } = req.query; // 기본값 설정
  page = parseInt(page);
  size = parseInt(size);

  const offset = (page - 1) * size;

  const sql = `SELECT * FROM books LIMIT ? OFFSET ?`;
  conn.query(sql, [size, offset], (err, results) => {
    if (err) {
      return res.status(500).json({ error: "Database error" });
    }
    res.status(200).json(results);
  });
};
  • 클라이언트 요청: GET /api/books?page=2&size=10
  • 실행 쿼리:
    SELECT * FROM books LIMIT 10 OFFSET 10;

추가 사항

1. 총 데이터 개수 반환

페이징 결과와 함께 전체 데이터 개수를 반환하면 클라이언트에서 총 페이지 수를 계산할 수 있습니다:

const sqlCount = `SELECT COUNT(*) as total FROM books`;
const sqlPaged = `SELECT * FROM books LIMIT ? OFFSET ?`;

conn.query(sqlCount, (err, countResults) => {
  if (err) return res.status(500).json({ error: "Database error" });

  const total = countResults[0].total;
  const totalPages = Math.ceil(total / size);

  conn.query(sqlPaged, [size, offset], (err, results) => {
    if (err) return res.status(500).json({ error: "Database error" });

    res.status(200).json({
      data: results,
      total,
      totalPages,
      currentPage: page,
    });
  });
});

페이징의 장단점

장점

  • 대량의 데이터 처리 성능 향상
  • 네트워크 및 서버 부하 감소
  • 클라이언트의 빠른 응답 속도 제공

단점

  • 페이지가 많아질수록 OFFSET 계산 비용이 커질 수 있음.
  • 데이터가 실시간으로 추가/삭제되면 페이징 결과의 정합성 문제가 발생할 수 있음.
    • 이를 해결하기 위해 Keyset Pagination 또는 Cursor Pagination 같은 방법을 사용할 수 있음.

고급 페이징: Keyset Pagination

Keyset Pagination은 OFFSET 대신 정렬된 특정 키(예: id)를 기반으로 페이지를 가져오는 방식입니다.

sql
코드 복사
SELECT * FROM books
WHERE id > ?
ORDER BY id ASC
LIMIT 10;
  • 이전에 받은 마지막 데이터의 id를 클라이언트가 저장하고, 다음 요청에 이를 전달하여 페이지를 가져옵니다.
  • 장점: 데이터가 많아도 성능 저하가 적음.
  • 단점: 특정 상황(예: 복잡한 필터링)에서는 구현이 어려울 수 있음.

실제 내가 진행하는 플젝에 적용해보자

const conn = require("../db");
const { StatusCodes } = require("http-status-codes");

// (카테고리별 , 신간 여부) 전체 도서 목록 조회
const allBooks = (req, res) => {
  let { category_id, newBook, limit, currentPage } = req.query;

  // limit : page당 보여줄 갯 수
  // currentPage : 현재 페이지(숫자)
  // offset : (current-1)*limit

  let offset = limit * (currentPage - 1);

  let sql = `SELECT * FROM books `;
  let values = [];

  if (category_id && newBook) {
    // category별 신간
    sql += ` WHERE category_id = ? AND pub_date BETWEEN DATE_SUB(CURDATE(), INTERVAL 1 month) AND NOW()`;
    values = [category_id];
  } else if (category_id) {
    // category별
    sql += ` WHERE category_id = ? `;
    values = [category_id];
  } else if (newBook) {
    // 신간
    sql +=
      " WHERE pub_date BETWEEN DATE_SUB(CURDATE(), INTERVAL 1 month) AND NOW()";
  }

  sql += ` LIMIT ? OFFSET ?`;
  values.push(parseInt(limit), offset);

  conn.query(sql, values, (err, results) => {
    if (err) {
      console.error(err);
      return res
        .status(StatusCodes.BAD_GATEWAY)
        .json({ error: "데이터베이스 오류입니다." });
    }
    if (results.length) {
      return res.status(StatusCodes.OK).json(results);
    } else {
      return res.status(StatusCodes.NOT_FOUND).end();
    }
  });
};


module.exports = { allBooks, bookDetail };

코들 로직 설명

  1. let sqlSELECT * FROM books으로 설정
  2. values라는 빈 배열 선언
  3. if...else...else if()문을 사용해서 request의 쿼리값에 따라 동적으로 WHERE조건을 values배열에 추가
    4-1. sql += 'LIMIT ? OFFSET ?' 으로 데이터베이스에 실행할 쿼리문 최종 추가
    4-2. values.push(parseInt(limit),offset);으로 LIMIT ? OFFSET ?에 사용할 조건도 추가
  4. conn.query()실행

여기서 주의할 사항들 !

  1. 쿼리문은 띄워쓰기를 잘해야합니다. let sql구문을 보면 끝에 공백 추가를 해야 질의가 들어갑니다.
    • 또한 sql += ' LIMIT ? OFFSET ? ';도 마찬가지고 앞에 공백 추가를 해야 합니다.
  2. 위 코드는 문법적으로 어렵지는 않습니다. 다만 논리적이지 못할 경우 워하는 값을 얻어내기 힘듭니다.
    • Q1. LIMIT, OFFSET은 질의문 마지막에 들어가야하는데 어떻게 if문을 분기처리해야하지?
    • Q2. category_id,newBook이라는 값이 있거나 없거나 하는 상황에 따라 어떻게 원하는 값을 얻어낼 수 있지?

위 예제 코드가 있어서 이해가 빨랐지만, 처음 로직을 만드려 했을 때는 상당히 헷갈렸던 거 같습니다..

0개의 댓글