πŸ” μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜ νŽ˜μ΄μ§• 처리 μ™„μ „ μ •λ³΅ν•˜κΈ°

λ‚˜κ·Όλ―ΌΒ·2025λ…„ 4μ›” 1일
0
post-thumbnail

μ•ˆλ…•ν•˜μ„Έμš”! μ˜€λŠ˜μ€ μ›Ή κ°œλ°œμ„ ν•˜λ‹€ 보면 ν”Όν•  수 μ—†λŠ” νŽ˜μ΄μ§• μ²˜λ¦¬μ— λŒ€ν•΄ μ™„λ²½ν•˜κ²Œ 정리해보렀고 ν•©λ‹ˆλ‹€. λŒ€λŸ‰μ˜ 데이터λ₯Ό 효율적으둜 보여주기 μœ„ν•œ νŽ˜μ΄μ§• μ²˜λ¦¬λŠ” μ–΄λ–»κ²Œ ν•˜λ©΄ λ κΉŒμš”? ν”„λ‘ νŠΈμ—”λ“œλΆ€ν„° λ°±μ—”λ“œ, DBκΉŒμ§€ 전체 흐름을 μ‚΄νŽ΄λ³΄κ³  μ΅œμ ν™” λ°©λ²•κΉŒμ§€ ν•œ λ²ˆμ— μ•Œμ•„λ³΄κ² μŠ΅λ‹ˆλ‹€.

πŸ“š λͺ©μ°¨

  1. νŽ˜μ΄μ§• μ²˜λ¦¬κ°€ ν•„μš”ν•œ 이유
  2. λ°μ΄ν„°λ² μ΄μŠ€ νŽ˜μ΄μ§• κ΅¬ν˜„ν•˜κΈ°
  3. μ›Ή νŽ˜μ΄μ§• 처리의 전체 흐름
  4. νŽ˜μ΄μ§• μ΅œμ ν™” μ „λž΅
  5. νŽ˜μ΄μ§• λŒ€μ•ˆ 기법듀

νŽ˜μ΄μ§• μ²˜λ¦¬κ°€ ν•„μš”ν•œ 이유

SNS ν”Όλ“œ, μ‡Όν•‘λͺ° μƒν’ˆ λͺ©λ‘, κ²Œμ‹œνŒ... 이런 μ„œλΉ„μŠ€λ“€μ˜ 곡톡점은 λ­˜κΉŒμš”? λ°”λ‘œ λŒ€μš©λŸ‰ 데이터λ₯Ό λ‹€λ£¬λ‹€λŠ” μ μž…λ‹ˆλ‹€.

예λ₯Ό λ“€μ–΄λ³ΌκΉŒμš”? λ§Œμ•½ μ‡Όν•‘λͺ°μ— μƒν’ˆμ΄ 10,000개 μžˆλ‹€λ©΄, 이걸 ν•œ νŽ˜μ΄μ§€μ— λ‹€ 보여쀀닀고 μƒμƒν•΄λ³΄μ„Έμš”. 😱

  • μ„œλ²„ λΆ€ν•˜: 10,000개 데이터λ₯Ό ν•œ λ²ˆμ— μ‘°νšŒν•˜κ³  μ „μ†‘ν•˜λ©΄ μ„œλ²„μ— λΆ€λ‹΄
  • λ„€νŠΈμ›Œν¬ νŠΈλž˜ν”½: λΆˆν•„μš”ν•˜κ²Œ λ§Žμ€ 데이터 μ „μ†‘μœΌλ‘œ λ„€νŠΈμ›Œν¬ λ‚­λΉ„
  • μ‚¬μš©μž κ²½ν—˜: ν•œ νŽ˜μ΄μ§€μ— 10,000개 μƒν’ˆμ΄ λ‘œλ”©λ˜λŠ” λ™μ•ˆ μ‚¬μš©μžλŠ” κΈ°λ‹€λ €μ•Ό 함
  • λ Œλ”λ§ λΆ€ν•˜: λΈŒλΌμš°μ €κ°€ 10,000개 μš”μ†Œλ₯Ό λ Œλ”λ§ν•˜λŠλΌ 느렀짐

이런 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ νŽ˜μ΄μ§• μ²˜λ¦¬λŠ” ν•„μˆ˜μž…λ‹ˆλ‹€.

λ°μ΄ν„°λ² μ΄μŠ€ νŽ˜μ΄μ§• κ΅¬ν˜„ν•˜κΈ°

LIMIT와 OFFSET의 λ§ˆλ²• ✨

MySQLμ΄λ‚˜ PostgreSQL 같은 λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œλŠ” LIMIT와 OFFSET ꡬ문으둜 νŽ˜μ΄μ§•μ„ κ΅¬ν˜„ν•©λ‹ˆλ‹€.

SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 20;

이 μΏΌλ¦¬λŠ” 무슨 μ˜λ―ΈμΌκΉŒμš”?

  • LIMIT 10: μ΅œλŒ€ 10개 ν–‰λ§Œ 가져와!
  • OFFSET 20: 처음 20κ°œλŠ” κ±΄λ„ˆλ›°κ³  κ·Έ λ‹€μŒλΆ€ν„° 가져와!

μ‰½κ²Œ μƒκ°ν•˜λ©΄:

  • 1νŽ˜μ΄μ§€(1~10번): LIMIT 10 OFFSET 0
  • 2νŽ˜μ΄μ§€(11~20번): LIMIT 10 OFFSET 10
  • 3νŽ˜μ΄μ§€(21~30번): 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;

μ›Ή νŽ˜μ΄μ§• 처리의 전체 흐름

νŽ˜μ΄μ§•μ΄ μ–΄λ–»κ²Œ λ™μž‘ν•˜λŠ”μ§€ 전체 흐름을 λ”°λΌκ°€λ³ΌκΉŒμš”?

μ‚¬μš©μž β†’ ν”„λ‘ νŠΈμ—”λ“œ β†’ λ°±μ—”λ“œ β†’ DB β†’ λ°±μ—”λ“œ β†’ ν”„λ‘ νŠΈμ—”λ“œ β†’ μ‚¬μš©μž

1. μ‚¬μš©μž μš”μ²­ 단계 πŸ§‘β€πŸ’»

μ‚¬μš©μžκ°€ νŽ˜μ΄μ§€λ„€μ΄μ…˜ 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);
  });

2. λ°±μ—”λ“œ 처리 단계 βš™οΈ

// μš”μ²­μ—μ„œ νŽ˜μ΄μ§€ 정보 μΆ”μΆœ
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

3. DB 쿼리 단계 πŸ’Ύ

// 데이터 쑰회 쿼리
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";

4. λ°±μ—”λ“œ 응닡 ꡬ성 단계 πŸ“¦

// 데이터 κ°€μ Έμ˜€κΈ°
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);

5. ν”„λ‘ νŠΈμ—”λ“œ κ²°κ³Ό ν‘œμ‹œ 단계 🎨

// μƒν’ˆ λͺ©λ‘ λ Œλ”λ§
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, 'λ‹€μŒ');
  }
}

νŽ˜μ΄μ§• μ΅œμ ν™” μ „λž΅

μ„±λŠ₯ μ΅œμ ν™” πŸš€

  1. 인덱슀 ν™œμš©ν•˜κΈ°

    -- id에 μΈλ±μŠ€κ°€ μžˆμ–΄μ•Ό ORDER BYκ°€ λΉ λ₯΄κ²Œ λ™μž‘ν•©λ‹ˆλ‹€
    CREATE INDEX idx_products_id ON products(id);
  2. ν•„μš”ν•œ 컬럼만 μ‘°νšŒν•˜κΈ°

    -- SELECT *보닀 ν•„μš”ν•œ 컬럼만 λͺ…μ‹œν•˜λŠ” 게 μ’‹μ•„μš”
    SELECT id, name, price, thumbnail FROM products 
    ORDER BY id LIMIT 10 OFFSET 20;
  3. κ²°κ³Ό μΊμ‹±ν•˜κΈ°

    // 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;
  4. 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보닀 훨씬 νš¨μœ¨μ μ΄μ—μš”
  • μƒˆ 데이터가 μΆ”κ°€λ˜λ”λΌλ„ νŽ˜μ΄μ§€ κ²°κ³Όκ°€ 밀리지 μ•Šμ•„μš”

단점:

  • νŠΉμ • νŽ˜μ΄μ§€ 번호둜 λ°”λ‘œ 이동할 수 μ—†μ–΄μš”
  • κ΅¬ν˜„μ΄ μƒλŒ€μ μœΌλ‘œ λ³΅μž‘ν•΄μš”

마무리

μ˜€λŠ˜μ€ μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ νŽ˜μ΄μ§• 처리λ₯Ό κ΅¬ν˜„ν•˜λŠ” 방법에 λŒ€ν•΄ μžμ„Ένžˆ μ•Œμ•„λ΄€μŠ΅λ‹ˆλ‹€. νŽ˜μ΄μ§•μ€ λ‹¨μˆœν•΄ λ³΄μ΄μ§€λ§Œ μ‹€μ œλ‘œλŠ” ν”„λ‘ νŠΈμ—”λ“œ, λ°±μ—”λ“œ, λ°μ΄ν„°λ² μ΄μŠ€κ°€ λͺ¨λ‘ μ—°κ²°λœ λ³΅μž‘ν•œ μž‘μ—…μž…λ‹ˆλ‹€.

기본적인 OFFSET/LIMIT 방식뢀터 μ΅œμ‹  νŠΈλ Œλ“œμΈ λ¬΄ν•œ 슀크둀과 μ»€μ„œ 기반 νŽ˜μ΄μ§•κΉŒμ§€, μ—¬λŸ¬λΆ„μ˜ μ„œλΉ„μŠ€μ— λ§žλŠ” νŽ˜μ΄μ§• μ „λž΅μ„ μ„ νƒν•˜μ‹œκΈΈ λ°”λžλ‹ˆλ‹€. 무엇보닀 μ‚¬μš©μž κ²½ν—˜κ³Ό μ„œλ²„ μ„±λŠ₯ μ‚¬μ΄μ˜ κ· ν˜•μ„ 잘 μ°ΎλŠ” 것이 μ€‘μš”ν•©λ‹ˆλ‹€!

λ‹€μŒμ—λŠ” 더 μ‹¬ν™”λœ νŽ˜μ΄μ§• κΈ°λ²•μ΄λ‚˜ λŒ€μš©λŸ‰ 데이터 처리 방법에 λŒ€ν•΄ 닀루도둝 ν•˜κ² μŠ΅λ‹ˆλ‹€. κΆκΈˆν•œ μ μ΄λ‚˜ 의견이 μžˆμœΌμ‹œλ©΄ λŒ“κΈ€λ‘œ λ‚¨κ²¨μ£Όμ„Έμš”! 😊


참고 자료

profile
개발 곡뢀쀑인 ν•™μƒμž…λ‹ˆλ‹€~

0개의 λŒ“κΈ€