μλ νμΈμ! μ€λμ μΉ κ°λ°μ νλ€ λ³΄λ©΄ νΌν μ μλ νμ΄μ§ μ²λ¦¬μ λν΄ μλ²½νκ² μ 리ν΄λ³΄λ €κ³ ν©λλ€. λλμ λ°μ΄ν°λ₯Ό ν¨μ¨μ μΌλ‘ 보μ¬μ£ΌκΈ° μν νμ΄μ§ μ²λ¦¬λ μ΄λ»κ² νλ©΄ λ κΉμ? νλ‘ νΈμλλΆν° λ°±μλ, DBκΉμ§ μ 체 νλ¦μ μ΄ν΄λ³΄κ³ μ΅μ ν λ°©λ²κΉμ§ ν λ²μ μμλ³΄κ² μ΅λλ€.
SNS νΌλ, μΌνλͺ° μν λͺ©λ‘, κ²μν... μ΄λ° μλΉμ€λ€μ 곡ν΅μ μ λκΉμ? λ°λ‘ λμ©λ λ°μ΄ν°λ₯Ό λ€λ£¬λ€λ μ μ λλ€.
μλ₯Ό λ€μ΄λ³ΌκΉμ? λ§μ½ μΌνλͺ°μ μνμ΄ 10,000κ° μλ€λ©΄, μ΄κ±Έ ν νμ΄μ§μ λ€ λ³΄μ¬μ€λ€κ³ μμν΄λ³΄μΈμ. π±
μ΄λ° λ¬Έμ λ₯Ό ν΄κ²°νκΈ° μν΄ νμ΄μ§ μ²λ¦¬λ νμμ λλ€.
MySQLμ΄λ PostgreSQL κ°μ λ°μ΄ν°λ² μ΄μ€μμλ LIMIT
μ OFFSET
ꡬ문μΌλ‘ νμ΄μ§μ ꡬνν©λλ€.
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 20;
μ΄ μΏΌλ¦¬λ λ¬΄μ¨ μλ―ΈμΌκΉμ?
LIMIT 10
: μ΅λ 10κ° νλ§ κ°μ Έμ!OFFSET 20
: μ²μ 20κ°λ 건λλ°κ³ κ·Έ λ€μλΆν° κ°μ Έμ!μ½κ² μκ°νλ©΄:
LIMIT 10 OFFSET 0
LIMIT 10 OFFSET 10
LIMIT 10 OFFSET 20
μ΄κ±Έ μμμΌλ‘ νννλ©΄ μ΄λ κ² λ©λλ€:
OFFSET = (νμ΄μ§ λ²νΈ - 1) Γ νμ΄μ§ ν¬κΈ°
LIMIT = νμ΄μ§ ν¬κΈ°
κ° λ°μ΄ν°λ² μ΄μ€λ§λ€ νμ΄μ§ κ΅¬λ¬Έμ΄ μ‘°κΈμ© λ€λ¦ λλ€:
MySQL/PostgreSQL
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 20;
-- λλ
SELECT * FROM products ORDER BY id LIMIT 10, 20; -- (LIMIT κ°μ, OFFSET)
Oracle
SELECT * FROM (
SELECT rownum as rnum, p.*
FROM products p
WHERE rownum <= 30
) WHERE rnum > 20;
SQL Server
SELECT * FROM products
ORDER BY id
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
νμ΄μ§μ΄ μ΄λ»κ² λμνλμ§ μ 체 νλ¦μ λ°λΌκ°λ³ΌκΉμ?
μ¬μ©μκ° νμ΄μ§λ€μ΄μ UIμμ "3νμ΄μ§" λ²νΌμ ν΄λ¦νλ©΄:
// Fetch APIλ‘ μμ² λ³΄λ΄κΈ°
fetch('/api/products?page=3&size=10')
.then(response => response.json())
.then(data => {
// λ°μ λ°μ΄ν°λ‘ νλ©΄ μ
λ°μ΄νΈνκΈ°
renderProducts(data.products);
renderPagination(data.currentPage, data.totalPages);
});
// μμ²μμ νμ΄μ§ μ 보 μΆμΆ
int page = Integer.parseInt(request.getParameter("page"));
int size = Integer.parseInt(request.getParameter("size"));
// μ ν¨μ± κ²μ¬ (μμλ λ무 ν° κ° λ°©μ§)
if (page < 1) page = 1;
if (size < 1 || size > 100) size = 10;
// OFFSET κ³μ°
int offset = (page - 1) * size; // 3νμ΄μ§λ©΄ offset = 20
// λ°μ΄ν° μ‘°ν 쿼리
String query = "SELECT * FROM products ORDER BY id LIMIT ? OFFSET ?";
PreparedStatement stmt = connection.prepareStatement(query);
stmt.setInt(1, size); // 10
stmt.setInt(2, offset); // 20
// μ 체 μν κ°μλ μμμΌ μ΄ νμ΄μ§ μλ₯Ό κ³μ°ν μ μμ΄μ
String countQuery = "SELECT COUNT(*) FROM products";
// λ°μ΄ν° κ°μ Έμ€κΈ°
List<Product> products = new ArrayList<>();
while (rs.next()) {
// ResultSetμμ μν μ 보 μΆμΆ
Product product = new Product();
product.setId(rs.getLong("id"));
product.setName(rs.getString("name"));
products.add(product);
}
// μ 체 κ°μλ‘ νμ΄μ§ μ 보 κ³μ°
int totalItems = countRs.getInt(1);
int totalPages = (int) Math.ceil((double) totalItems / size);
// JSON μλ΅ κ΅¬μ±
Map<String, Object> response = new HashMap<>();
response.put("products", products);
response.put("currentPage", page);
response.put("totalPages", totalPages);
response.put("totalItems", totalItems);
return ResponseEntity.ok(response);
// μν λͺ©λ‘ λ λλ§
function renderProducts(products) {
const container = document.getElementById('products-container');
container.innerHTML = '';
products.forEach(product => {
const productEl = document.createElement('div');
productEl.className = 'product-item';
productEl.innerHTML = `
<h3>${product.name}</h3>
<p>${product.price}μ</p>
`;
container.appendChild(productEl);
});
}
// νμ΄μ§λ€μ΄μ
UI λ λλ§
function renderPagination(currentPage, totalPages) {
const paginationEl = document.getElementById('pagination');
paginationEl.innerHTML = '';
// μ΄μ νμ΄μ§ λ²νΌ
if (currentPage > 1) {
addPageButton(paginationEl, currentPage - 1, 'μ΄μ ');
}
// νμ΄μ§ λ²νΈ λ²νΌλ€ (νμ¬ κΈ°μ€ μ’μ° 2κ°μ©)
const startPage = Math.max(1, currentPage - 2);
const endPage = Math.min(totalPages, startPage + 4);
for (let i = startPage; i <= endPage; i++) {
addPageButton(paginationEl, i, i.toString(), i === currentPage);
}
// λ€μ νμ΄μ§ λ²νΌ
if (currentPage < totalPages) {
addPageButton(paginationEl, currentPage + 1, 'λ€μ');
}
}
μΈλ±μ€ νμ©νκΈ°
-- idμ μΈλ±μ€κ° μμ΄μΌ ORDER BYκ° λΉ λ₯΄κ² λμν©λλ€
CREATE INDEX idx_products_id ON products(id);
νμν 컬λΌλ§ μ‘°ννκΈ°
-- SELECT *λ³΄λ€ νμν 컬λΌλ§ λͺ
μνλ κ² μ’μμ
SELECT id, name, price, thumbnail FROM products
ORDER BY id LIMIT 10 OFFSET 20;
κ²°κ³Ό μΊμ±νκΈ°
// Redis κ°μ μΊμμ νμ΄μ§ κ²°κ³Όλ₯Ό μ μ₯
String cacheKey = "products:page:" + page + ":size:" + size;
// μΊμμ μμΌλ©΄ λ°λ‘ λ°ν
if (redisCache.hasKey(cacheKey)) {
return redisCache.get(cacheKey);
}
// μμΌλ©΄ DBμμ μ‘°ν ν μΊμμ μ μ₯
Map<String, Object> result = queryDatabase(page, size);
redisCache.set(cacheKey, result, 5, TimeUnit.MINUTES);
return result;
COUNT 쿼리 μ΅μ ν
// λͺ¨λ νμ΄μ§ μμ²λ§λ€ COUNT 쿼리λ₯Ό μ€ννλ 건 λΉν¨μ¨μ
// μΊμ±νκ±°λ 첫 νμ΄μ§μμλ§ μ€ννλ μ λ΅μ κ³ λ €ν΄λ³΄μΈμ
// μ‘΄μ¬νμ§ μλ νμ΄μ§ μμ² μ (νμ΄μ§ λ²νΈκ° μ΄ νμ΄μ§ μλ³΄λ€ ν° κ²½μ°)
if (page > totalPages && totalPages > 0) {
// 1. λ§μ§λ§ νμ΄μ§λ‘ 리λ€μ΄λ νΈ
return "redirect:/products?page=" + totalPages;
// λλ 2. λΉ κ²°κ³Όμ ν¨κ» μ ν¨ν νμ΄μ§ λ²μ μ 보 μ 곡
Map<String, Object> response = new HashMap<>();
response.put("products", Collections.emptyList());
response.put("currentPage", page);
response.put("totalPages", totalPages);
response.put("error", "μμ²ν νμ΄μ§κ° λ²μλ₯Ό λ²μ΄λ¬μ΅λλ€.");
return ResponseEntity.ok(response);
}
μΈμ€νκ·Έλ¨μ΄λ νμ΄μ€λΆμ²λΌ μ€ν¬λ‘€μ λ΄λ¦¬λ©΄ κ³μ 컨ν μΈ κ° λ‘λλλ λ°©μμ λλ€.
window.addEventListener('scroll', function() {
// νμ΄μ§ νλ¨μ λλ¬νλ©΄
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 100) {
if (!isLoading) {
isLoading = true;
// λ€μ νμ΄μ§ λ°μ΄ν° λ‘λ
fetch(`/api/products?page=${currentPage + 1}&size=10`)
.then(response => response.json())
.then(data => {
// κΈ°μ‘΄ λͺ©λ‘μ μ λ°μ΄ν° μΆκ°
appendProducts(data.products);
currentPage++;
isLoading = false;
});
}
}
});
μ₯μ :
λ¨μ :
OFFSET λμ λ§μ§λ§μΌλ‘ λ³Έ νλͺ©μ IDλ νμμ€ν¬νλ₯Ό κΈ°μ€μΌλ‘ λ€μ λ°μ΄ν°λ₯Ό κ°μ Έμ€λ λ°©μμ λλ€.
-- 첫 νμ΄μ§
SELECT * FROM products
ORDER BY created_at DESC
LIMIT 10;
-- λ€μ νμ΄μ§ (λ§μ§λ§μΌλ‘ λ³Έ νλͺ©μ created_at κ°μ΄ '2023-04-15 14:30:00'μΌ λ)
SELECT * FROM products
WHERE created_at < '2023-04-15 14:30:00'
ORDER BY created_at DESC
LIMIT 10;
μ₯μ :
λ¨μ :
μ€λμ μΉ μ ν리μΌμ΄μ μμ νμ΄μ§ μ²λ¦¬λ₯Ό ꡬννλ λ°©λ²μ λν΄ μμΈν μμλ΄€μ΅λλ€. νμ΄μ§μ λ¨μν΄ λ³΄μ΄μ§λ§ μ€μ λ‘λ νλ‘ νΈμλ, λ°±μλ, λ°μ΄ν°λ² μ΄μ€κ° λͺ¨λ μ°κ²°λ 볡μ‘ν μμ μ λλ€.
κΈ°λ³Έμ μΈ OFFSET/LIMIT λ°©μλΆν° μ΅μ νΈλ λμΈ λ¬΄ν μ€ν¬λ‘€κ³Ό 컀μ κΈ°λ° νμ΄μ§κΉμ§, μ¬λ¬λΆμ μλΉμ€μ λ§λ νμ΄μ§ μ λ΅μ μ ννμκΈΈ λ°λλλ€. 무μλ³΄λ€ μ¬μ©μ κ²½νκ³Ό μλ² μ±λ₯ μ¬μ΄μ κ· νμ μ μ°Ύλ κ²μ΄ μ€μν©λλ€!
λ€μμλ λ μ¬νλ νμ΄μ§ κΈ°λ²μ΄λ λμ©λ λ°μ΄ν° μ²λ¦¬ λ°©λ²μ λν΄ λ€λ£¨λλ‘ νκ² μ΅λλ€. κΆκΈν μ μ΄λ μκ²¬μ΄ μμΌμλ©΄ λκΈλ‘ λ¨κ²¨μ£ΌμΈμ! π